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:
ZacharyZhang-NY committed 2026-03-27 22:12:00 -04:00
1 parent 2ed5dd3388
commit 06ea4c6aec
99 files changed
+1265 -1265

No files matched your search

+73 -73
View File
@@ -693,8 +693,8 @@ export function App() {
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className="flex min-h-screen flex-col items-center justify-center gap-3">
<CircleNotch size={24} className="animate-spin text-[color:var(--accent)]" />
<span className="text-[13px] text-[color:var(--muted)]">{t("app.loading")}</span>
<CircleNotch size={24} className="animate-spin text-primary" />
<span className="text-[13px] text-muted-foreground">{t("app.loading")}</span>
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
@@ -726,14 +726,14 @@ export function App() {
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className={`grid h-[100dvh] overflow-hidden bg-[color:var(--bg)] ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line-strong)] bg-[color:var(--panel)]">
<div className={`grid h-[100dvh] overflow-hidden bg-background ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-border bg-card">
{sidebarCollapsed ? (
<div className="sidebar-drag flex items-center justify-center px-2 pb-3 pt-[38px]">
<button
aria-label="Open command palette"
title="Command palette"
className="focus-ring flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[6px] border border-[color:var(--line)] text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground transition hover:bg-muted hover:text-foreground"
onClick={() => setCommandPaletteOpen(true)}
>
<Command size={14} />
@@ -744,32 +744,32 @@ export function App() {
<div className="relative min-w-0 flex-1">
<button
type="button"
className="focus-ring flex w-full items-center gap-2 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1.5 text-left text-[13px] font-medium text-[color:var(--text)] 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-2 rounded-lg border border-border bg-card px-2.5 py-1.5 text-left text-[13px] font-medium text-foreground transition hover:bg-muted"
onClick={() => setCompanySwitcherOpen(!companySwitcherOpen)}
>
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span>
<CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" />
<CaretUpDown size={14} className="shrink-0 text-muted-foreground" />
</button>
{companySwitcherOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => { setCompanySwitcherOpen(false); setConfirmDeleteCompanyId(null); }} />
<div className="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-lg">
<div className="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-lg border border-border bg-card shadow-lg">
<div className="max-h-[240px] overflow-y-auto py-1">
{snapshot.companies.map((company) => (
<div key={company.id} className="group flex items-center">
{confirmDeleteCompanyId === company.id ? (
<div className="flex w-full items-center gap-2 px-3 py-2">
<span className="flex-1 truncate text-[12px] text-[color:var(--danger)]">Delete {company.name}?</span>
<span className="flex-1 truncate text-[12px] text-destructive">Delete {company.name}?</span>
<button
type="button"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-destructive transition hover:bg-destructive hover:text-primary-foreground"
onClick={() => void deleteCompany(company.id)}
>
Yes
</button>
<button
type="button"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
className="rounded px-2 py-0.5 text-[11px] font-semibold text-muted-foreground transition hover:bg-muted"
onClick={() => setConfirmDeleteCompanyId(null)}
>
No
@@ -779,17 +779,17 @@ export function App() {
<>
<button
type="button"
className={`flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2 text-left text-[13px] transition hover:bg-[color:var(--panel-soft)] ${
company.id === currentCompany?.id ? "font-semibold text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
className={`flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2 text-left text-[13px] transition hover:bg-muted ${
company.id === currentCompany?.id ? "font-semibold text-foreground" : "text-muted-foreground"
}`}
onClick={() => { void setCompany(company.id); setCompanySwitcherOpen(false); }}
>
<span className="w-4 shrink-0">{company.id === currentCompany?.id ? <Check size={14} weight="bold" className="text-[color:var(--accent)]" /> : null}</span>
<span className="w-4 shrink-0">{company.id === currentCompany?.id ? <Check size={14} weight="bold" className="text-primary" /> : null}</span>
<span className="min-w-0 flex-1 truncate">{company.name}</span>
</button>
<button
type="button"
className="mr-2 rounded p-1 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)] group-hover:opacity-100"
className="mr-2 rounded p-1 text-muted-foreground opacity-0 transition hover:bg-destructive hover:text-primary-foreground group-hover:opacity-100"
title={`Delete ${company.name}`}
onClick={() => setConfirmDeleteCompanyId(company.id)}
>
@@ -800,10 +800,10 @@ export function App() {
</div>
))}
</div>
<div className="border-t border-[color:var(--line)]">
<div className="border-t border-border">
<button
type="button"
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-[13px] text-[color:var(--muted-strong)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-[13px] text-muted-foreground transition hover:bg-muted hover:text-foreground"
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
>
<Plus size={14} />
@@ -816,7 +816,7 @@ export function App() {
</div>
<button
aria-label="Open command palette"
className="focus-ring flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[6px] border border-[color:var(--line)] text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground transition hover:bg-muted hover:text-foreground"
onClick={() => setCommandPaletteOpen(true)}
>
<Command size={14} />
@@ -839,7 +839,7 @@ export function App() {
))}
</div>
</nav>
<div className={`border-t border-[color:var(--line)] py-2 ${sidebarCollapsed ? "px-1.5" : "px-3"}`}>
<div className={`border-t border-border py-2 ${sidebarCollapsed ? "px-1.5" : "px-3"}`}>
<SectionButton
active={section === "settings"}
label="Settings"
@@ -850,7 +850,7 @@ export function App() {
<button
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
className={`focus-ring mt-1 flex w-full items-center rounded-[6px] py-[7px] text-[color:var(--muted)] transition duration-150 hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] ${sidebarCollapsed ? "justify-center p-2" : "gap-2.5 px-3"}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 mt-1 flex w-full items-center rounded-lg py-[7px] text-muted-foreground transition duration-150 hover:bg-muted hover:text-foreground ${sidebarCollapsed ? "justify-center p-2" : "gap-2.5 px-3"}`}
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
>
<SidebarSimple size={sidebarCollapsed ? 18 : 17} />
@@ -860,9 +860,9 @@ export function App() {
</aside>
<main aria-label="Content" className="flex min-w-0 flex-col overflow-hidden">
<header className="sidebar-drag shrink-0 border-b border-[color:var(--line)] bg-[color:var(--bg)]">
<header className="sidebar-drag shrink-0 border-b border-border bg-background">
<div className="mx-auto flex w-full max-w-[1040px] items-start justify-between gap-4 px-6 pb-5 pt-6 lg:gap-6 lg:px-10 lg:pt-8">
<h1 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">
<h1 className="text-[28px] font-bold tracking-[-0.02em] text-foreground">
{navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
</h1>
<div className="flex shrink-0 items-center gap-3 lg:gap-5">
@@ -870,7 +870,7 @@ export function App() {
type="button"
aria-label={consoleOpen ? "Hide console" : "Show console"}
title={consoleOpen ? "Hide console" : "Show console"}
className="focus-ring inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex items-center gap-2 py-1 text-[12px] font-semibold text-muted-foreground transition hover:text-foreground"
onClick={() => setConsoleOpen(!consoleOpen)}
>
<TerminalWindow size={16} />
@@ -884,7 +884,7 @@ export function App() {
<div className={section === "communication" ? "flex min-h-0 flex-1 flex-col" : "mx-auto flex w-full max-w-[1040px] flex-col px-6 py-6 lg:px-10 lg:py-8"}>
{error ? (
<div className={`flex items-center gap-3 rounded-[8px] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)] ${section === "communication" ? "mx-6 mt-4" : "mb-6"}`}>
<div className={`flex items-center gap-3 rounded-lg bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive ${section === "communication" ? "mx-6 mt-4" : "mb-6"}`}>
<WarningCircle size={18} />
<span>{error}</span>
</div>
@@ -971,14 +971,14 @@ export function App() {
onClick={() => setSelectedConnectorId(connector.id)}
className={`cursor-pointer transition-colors duration-100 ${
selectedConnector.id === connector.id
? "bg-[color:var(--accent-soft)]"
: "hover:bg-[color:var(--panel-soft)]"
? "bg-accent"
: "hover:bg-muted"
}`}
>
<div className="flex items-center justify-between">
<div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{connector.label}</div>
<div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">{connector.command}</div>
<div className="text-[14px] font-medium text-foreground">{connector.label}</div>
<div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{connector.command}</div>
</div>
<StatusPill status={connector.status} />
</div>
@@ -1123,18 +1123,18 @@ export function App() {
title="Task queue"
action={
<div className="flex items-center gap-2">
<div className="flex rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="flex rounded-lg border border-border bg-card">
<button
type="button"
onClick={() => setTaskViewMode("list")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "list" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "list" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
>
<ListChecks size={14} />
</button>
<button
type="button"
onClick={() => setTaskViewMode("kanban")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "kanban" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "kanban" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
>
<Kanban size={14} />
</button>
@@ -1213,7 +1213,7 @@ export function App() {
documents={companyDocuments}
companyId={currentCompany!.id}
onDecide={async (state, note) => {
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control surface.` }));
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control bg-card border border-border shadow-sm.` }));
await refresh();
}}
onDeleted={async () => { setSelectedApprovalId(null); await refresh(); }}
@@ -1226,7 +1226,7 @@ export function App() {
{section === "costs" ? (
<div className="flex h-full flex-col">
{/* Tab bar */}
<div className="flex items-center gap-1 border-b border-[color:var(--line)] px-4 pt-2">
<div className="flex items-center gap-1 border-b border-border px-4 pt-2">
{(["costs", "policies", "incidents"] as const).map((tab) => (
<button
key={tab}
@@ -1234,8 +1234,8 @@ export function App() {
onClick={() => setCostsTab(tab)}
className={`rounded-t px-3 py-1.5 text-[13px] font-medium transition-colors ${
costsTab === tab
? "border-b-2 border-[color:var(--accent)] text-[color:var(--text)]"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
? "border-b-2 border-[color:var(--primary)] text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab === "costs" ? "Cost Entries" : tab === "policies" ? "Policies" : "Incidents"}
@@ -1467,12 +1467,12 @@ export function App() {
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={panelTransition}
className="shrink-0 overflow-hidden border-t border-[color:var(--line)] bg-[color:var(--panel)]"
className="shrink-0 overflow-hidden border-t border-border bg-card"
>
<div className="flex items-center justify-between px-5 py-2.5">
<div className="flex items-center gap-3">
<TerminalWindow size={15} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("console.title")}</span>
<TerminalWindow size={15} className="text-muted-foreground" />
<span className="text-[13px] font-semibold text-foreground">{t("console.title")}</span>
</div>
<div className="flex items-center gap-3">
<Select
@@ -1486,7 +1486,7 @@ export function App() {
/>
<button
type="button"
className="focus-ring rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg p-1 text-muted-foreground transition hover:text-foreground"
onClick={() => setConsoleOpen(false)}
aria-label="Close console"
>
@@ -1494,25 +1494,25 @@ export function App() {
</button>
</div>
</div>
<div className="grid border-t border-[color:var(--line)] xl:grid-cols-[240px_minmax(0,1fr)]">
<div className="border-b border-[color:var(--line)] px-5 py-3 xl:border-b-0 xl:border-r">
<div className="grid border-t border-border xl:grid-cols-[240px_minmax(0,1fr)]">
<div className="border-b border-border px-5 py-3 xl:border-b-0 xl:border-r">
{selectedRun ? (
<div className="space-y-2.5 text-[12px] text-[color:var(--muted-strong)]">
<div className="space-y-2.5 text-[12px] text-muted-foreground">
<div className="flex items-center justify-between">
<span>{t("common.status")}</span>
<StatusPill status={selectedRun.status} />
</div>
<div className="flex items-center justify-between">
<span>{t("common.started")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
<span className="font-mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>{t("common.finished")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
<span className="font-mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>{t("common.cost")}</span>
<span className="mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
<span className="font-mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
</div>
{selectedRun.logPath ? (
<div className="pt-1">
@@ -1521,11 +1521,11 @@ export function App() {
) : null}
</div>
) : (
<div className="py-4 text-center text-[12px] text-[color:var(--muted)]">{t("common.noRunSelected")}</div>
<div className="py-4 text-center text-[12px] text-muted-foreground">{t("common.noRunSelected")}</div>
)}
</div>
<div className="h-[200px] overflow-auto px-5 py-3">
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
<pre className="font-mono whitespace-pre-wrap text-[12px] leading-relaxed text-muted-foreground">
{selectedRun ? logBufferRef.current[selectedRun.id] ?? t("common.waitingForLog") : t("common.noRunSelected")}
</pre>
</div>
@@ -1541,7 +1541,7 @@ export function App() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-start justify-center bg-[color:var(--bg)]/72 px-4 pt-24 backdrop-blur-sm"
className="fixed inset-0 z-50 flex items-start justify-center bg-background/72 px-4 pt-24 backdrop-blur-sm"
onClick={() => { setCommandPaletteOpen(false); setCommandPaletteSearch(""); }}
>
<motion.div
@@ -1553,15 +1553,15 @@ export function App() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.98 }}
transition={panelTransition}
className="w-full max-w-[560px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
className="w-full max-w-[560px] rounded-lg border border-border bg-card px-5 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.08)]"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center gap-3">
<MagnifyingGlass size={18} className="text-[color:var(--accent)]" />
<MagnifyingGlass size={18} className="text-primary" />
<input
type="text"
aria-label="Search"
className="flex-1 bg-transparent text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] outline-none"
className="flex-1 bg-transparent text-[14px] text-foreground placeholder:text-muted-foreground outline-none"
placeholder={t("app.search")}
value={commandPaletteSearch}
onChange={(e) => setCommandPaletteSearch(e.target.value)}
@@ -1569,7 +1569,7 @@ export function App() {
/>
{commandPaletteSearch && (
<button
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]"
className="text-[12px] text-muted-foreground hover:text-foreground"
onClick={() => setCommandPaletteSearch("")}
>
{t("app.clear")}
@@ -1579,12 +1579,12 @@ export function App() {
{filteredNavItems.length > 0 ? (
<>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.sections")}</div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.sections")}</div>
<ListFrame>
{filteredNavItems.map((item) => (
<ListRow
key={item.id}
className={`py-3 ${paletteActionIndex.get(`section:${item.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`section:${item.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo(item.id);
setCommandPaletteOpen(false);
@@ -1593,11 +1593,11 @@ export function App() {
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<div className="flex items-center gap-3 text-foreground">
<item.icon size={16} />
<span className="text-[13px] tracking-[0.005em]">{item.label}</span>
</div>
<span className="mono text-[11px] text-[color:var(--muted)]">{item.id}</span>
<span className="font-mono text-[11px] text-muted-foreground">{item.id}</span>
</div>
</ListRow>
))}
@@ -1609,12 +1609,12 @@ export function App() {
<div className="mt-3 space-y-3">
{matchedAgents.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.agents")}</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.agents")}</div>
<ListFrame>
{matchedAgents.map((agent) => (
<ListRow
key={agent.id}
className={`py-3 ${paletteActionIndex.get(`agent:${agent.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`agent:${agent.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo("agents", agent.id);
setCommandPaletteOpen(false);
@@ -1623,10 +1623,10 @@ export function App() {
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<div className="flex items-center gap-3 text-foreground">
<UsersThree size={16} />
<span className="text-[13px] tracking-[0.005em]">{agent.name}</span>
<span className="text-[11px] text-[color:var(--muted)]">{agent.role}</span>
<span className="text-[11px] text-muted-foreground">{agent.role}</span>
</div>
<StatusPill status={agent.status} />
</div>
@@ -1638,12 +1638,12 @@ export function App() {
{matchedTasks.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.tasks")}</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.tasks")}</div>
<ListFrame>
{matchedTasks.map((task) => (
<ListRow
key={task.id}
className={`py-3 ${paletteActionIndex.get(`task:${task.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`task:${task.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo("tasks", task.id);
setCommandPaletteOpen(false);
@@ -1652,7 +1652,7 @@ export function App() {
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<div className="flex items-center gap-3 text-foreground">
<ListChecks size={16} />
<span className="text-[13px] tracking-[0.005em]">{task.title}</span>
</div>
@@ -1666,12 +1666,12 @@ export function App() {
{matchedRuns.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.runs")}</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.runs")}</div>
<ListFrame>
{matchedRuns.map((run) => (
<ListRow
key={run.id}
className={`py-3 ${paletteActionIndex.get(`run:${run.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`run:${run.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo("runs", run.id);
setCommandPaletteOpen(false);
@@ -1680,7 +1680,7 @@ export function App() {
}}
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex items-center gap-3 text-[color:var(--text)]">
<div className="min-w-0 flex items-center gap-3 text-foreground">
<TerminalWindow size={16} />
<span className="truncate text-[13px] tracking-[0.005em]">
{run.summary || runs.find((entry) => entry.id === run.id)?.id.slice(0, 8) || run.id.slice(0, 8)}
@@ -1696,12 +1696,12 @@ export function App() {
{matchedGoals.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.goals")}</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.goals")}</div>
<ListFrame>
{matchedGoals.map((goal) => (
<ListRow
key={goal.id}
className={`py-3 ${paletteActionIndex.get(`goal:${goal.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`goal:${goal.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo("goals", goal.id);
setCommandPaletteOpen(false);
@@ -1710,7 +1710,7 @@ export function App() {
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<div className="flex items-center gap-3 text-foreground">
<Target size={16} />
<span className="text-[13px] tracking-[0.005em]">{goal.title}</span>
</div>
@@ -1724,12 +1724,12 @@ export function App() {
{matchedProjects.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.projects")}</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("entity.projects")}</div>
<ListFrame>
{matchedProjects.map((project) => (
<ListRow
key={project.id}
className={`py-3 ${paletteActionIndex.get(`project:${project.id}`) === selectedPaletteIndex ? "bg-[color:var(--panel-soft)]" : ""}`}
className={`py-3 ${paletteActionIndex.get(`project:${project.id}`) === selectedPaletteIndex ? "bg-muted" : ""}`}
onClick={() => {
navigateTo("projects", project.id);
setCommandPaletteOpen(false);
@@ -1738,7 +1738,7 @@ export function App() {
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[color:var(--text)]">
<div className="flex items-center gap-3 text-foreground">
<Kanban size={16} />
<span className="text-[13px] tracking-[0.005em]">{project.name}</span>
</div>
@@ -1753,7 +1753,7 @@ export function App() {
) : null}
{paletteQuery && filteredNavItems.length === 0 && !hasEntityResults ? (
<div className="py-6 text-center text-[13px] text-[color:var(--muted)]">{t("app.noResults")} "{commandPaletteSearch}"</div>
<div className="py-6 text-center text-[13px] text-muted-foreground">{t("app.noResults")} "{commandPaletteSearch}"</div>
) : null}
</motion.div>
</motion.div>
@@ -67,15 +67,15 @@ export const AsciiAnimation = memo(function AsciiAnimation() {
{/* Fade masks */}
<div
className="absolute inset-x-0 top-0 z-10 h-32"
style={{ background: "linear-gradient(to bottom, var(--bg), transparent)" }}
style={{ background: "linear-gradient(to bottom, var(--background), transparent)" }}
/>
<div
className="absolute inset-x-0 bottom-0 z-10 h-32"
style={{ background: "linear-gradient(to top, var(--bg), transparent)" }}
style={{ background: "linear-gradient(to top, var(--background), transparent)" }}
/>
{/* Scrolling track — doubled for seamless loop */}
<div className="ascii-scroll-track whitespace-pre font-mono text-[11px] leading-[1.8] text-[color:var(--accent)] opacity-[0.08]">
<div className="ascii-scroll-track whitespace-pre font-mono text-[11px] leading-[1.8] text-primary opacity-[0.08]">
{BLOCK}
{"\n"}
{BLOCK}
@@ -27,54 +27,54 @@ export function ClaudeSettingsDialog({
currentLocale,
}: ClaudeSettingsDialogProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--bg)]/72 backdrop-blur-sm" onClick={onClose}>
<div className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/72 backdrop-blur-sm" onClick={onClose}>
<div className="w-full max-w-[420px] rounded-lg border border-border bg-card p-6 shadow-lg" onClick={(e) => e.stopPropagation()}>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[16px] font-semibold text-[color:var(--text)]">Claude Code {currentLocale === "zh" ? "设置" : "Settings"}</h3>
<button type="button" onClick={onClose} className="rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={16} /></button>
<h3 className="text-[16px] font-semibold text-foreground">Claude Code {currentLocale === "zh" ? "设置" : "Settings"}</h3>
<button type="button" onClick={onClose} className="rounded-lg p-1 text-muted-foreground transition hover:text-foreground"><X size={16} /></button>
</div>
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">API Key</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">API Key</label>
<input
type="password"
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeApiKey}
onChange={(e) => setClaudeApiKey(e.target.value)}
placeholder="sk-ant-..."
/>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}</div>
</div>
<div>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Base URL</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Base URL</label>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeBaseUrl}
onChange={(e) => setClaudeBaseUrl(e.target.value)}
placeholder="https://api.anthropic.com"
/>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}</div>
</div>
<div>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted-strong)]">Model</label>
<label className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Model</label>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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-3 py-2.5 text-[13px] text-foreground placeholder:text-muted-foreground"
value={claudeModel}
onChange={(e) => setClaudeModel(e.target.value)}
placeholder="claude-sonnet-4-20250514"
/>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{currentLocale === "zh" ? "留空使用连接器默认模型" : "Leave empty for connector default"}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{currentLocale === "zh" ? "留空使用连接器默认模型" : "Leave empty for connector default"}</div>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<button type="button" onClick={onClose} className="rounded-[8px] px-4 py-2 text-[13px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]">
<button type="button" onClick={onClose} className="rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition hover:text-foreground">
{currentLocale === "zh" ? "取消" : "Cancel"}
</button>
<button
type="button"
onClick={onSave}
disabled={saving}
className="rounded-[8px] bg-[color:var(--accent)] px-4 py-2 text-[13px] font-medium text-[color:var(--text-on-accent)] transition hover:opacity-90 disabled:opacity-50"
className="rounded-lg bg-primary px-4 py-2 text-[13px] font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-50"
>
{saving ? (currentLocale === "zh" ? "保存中..." : "Saving...") : (currentLocale === "zh" ? "保存" : "Save")}
</button>
@@ -58,15 +58,15 @@ export function CompanyStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[8px] p-7"
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabel1")} 1 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.step1Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-4">
<Input
@@ -91,15 +91,15 @@ export function CompanyStep({
label: connector.label,
}))}
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3 text-[12px] leading-relaxed text-muted-foreground">
{t("onboarding.connectorInfo")}
</div>
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
className="mb-1.5 block text-[12px] font-medium text-muted-foreground"
>
{t("onboarding.workspaceFolder")} <span className="font-normal text-[color:var(--muted)]">{t("onboarding.workspaceFolderOptional")}</span>
{t("onboarding.workspaceFolder")} <span className="font-normal text-muted-foreground">{t("onboarding.workspaceFolderOptional")}</span>
</label>
<div className="flex gap-3">
<Input
@@ -111,37 +111,37 @@ export function CompanyStep({
/>
<ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={onPickDirectory} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
<div className="mt-1.5 text-[11px] text-muted-foreground">
{t("onboarding.workspaceHelp")}
</div>
</div>
<div>
<div className="mb-2 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("onboarding.automationMode")}</div>
<div className="mb-2 text-[12px] font-medium text-muted-foreground">{t("onboarding.automationMode")}</div>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => { setReviewDeliverables(false); setAutoApproveHires(true) }}
className={`cursor-pointer rounded-[8px] border-2 px-4 py-3.5 text-left transition ${!reviewDeliverables ? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"}`}
className={`cursor-pointer rounded-lg border-2 px-4 py-3.5 text-left transition ${!reviewDeliverables ? "border-[color:var(--primary)] bg-accent" : "border-border bg-muted hover:border-[color:var(--muted-foreground)]"}`}
>
<Lightning size={20} weight={!reviewDeliverables ? "fill" : "regular"} className={!reviewDeliverables ? "text-[color:var(--accent)]" : "text-[color:var(--muted)]"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeFullAuto")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeFullAutoDesc")}</div>
<Lightning size={20} weight={!reviewDeliverables ? "fill" : "regular"} className={!reviewDeliverables ? "text-primary" : "text-muted-foreground"} />
<div className="mt-2 text-[13px] font-semibold text-foreground">{t("onboarding.modeFullAuto")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("onboarding.modeFullAutoDesc")}</div>
</button>
<button
type="button"
onClick={() => { setReviewDeliverables(true); setAutoApproveHires(false) }}
className={`cursor-pointer rounded-[8px] border-2 px-4 py-3.5 text-left transition ${reviewDeliverables ? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"}`}
className={`cursor-pointer rounded-lg border-2 px-4 py-3.5 text-left transition ${reviewDeliverables ? "border-[color:var(--primary)] bg-accent" : "border-border bg-muted hover:border-[color:var(--muted-foreground)]"}`}
>
<UserCircleCheck size={20} weight={reviewDeliverables ? "fill" : "regular"} className={reviewDeliverables ? "text-[color:var(--accent)]" : "text-[color:var(--muted)]"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeHumanReview")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeHumanReviewDesc")}</div>
<UserCircleCheck size={20} weight={reviewDeliverables ? "fill" : "regular"} className={reviewDeliverables ? "text-primary" : "text-muted-foreground"} />
<div className="mt-2 text-[13px] font-semibold text-foreground">{t("onboarding.modeHumanReview")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("onboarding.modeHumanReviewDesc")}</div>
</button>
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive">
{error}
</div>
) : null}
@@ -151,7 +151,7 @@ export function CompanyStep({
<button
type="button"
onClick={onBack}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="text-[12px] font-medium text-muted-foreground transition hover:text-foreground"
>
{t("onboarding.back")}
</button>
@@ -25,7 +25,7 @@ export function ConnectorReadiness({
}: ConnectorReadinessProps) {
return (
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.connectorReadiness")}
</div>
<div className="space-y-2.5">
@@ -37,25 +37,25 @@ export function ConnectorReadiness({
return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isChecking ? (
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted)] border-t-[color:var(--accent)]" />
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted-foreground)] border-t-[color:var(--primary)]" />
) : isReady ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
<CheckCircle size={16} weight="fill" className="shrink-0 text-green-600 dark:text-green-400" />
) : isMissing ? (
<XCircle size={16} weight="fill" className="shrink-0 text-[color:var(--muted)]" />
<XCircle size={16} weight="fill" className="shrink-0 text-muted-foreground" />
) : (
<WarningCircle size={16} weight="fill" className="shrink-0 text-[color:var(--warn)]" />
<WarningCircle size={16} weight="fill" className="shrink-0 text-yellow-600 dark:text-yellow-400" />
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={isReady ? "font-medium text-[color:var(--text)]" : isChecking ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
<span className={isReady ? "font-medium text-foreground" : isChecking ? "text-foreground" : "text-muted-foreground"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
<span className="text-[11px] text-muted-foreground">
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
<div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">
<div className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">
{issue}
</div>
) : null}
@@ -64,7 +64,7 @@ export function ConnectorReadiness({
<button
type="button"
onClick={(e) => { e.stopPropagation(); onOpenClaudeSettings(); }}
className="shrink-0 rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="shrink-0 rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
title={currentLocale === "zh" ? "Claude Code 设置" : "Claude Code Settings"}
>
<GearSix size={14} />
@@ -75,7 +75,7 @@ export function ConnectorReadiness({
})}
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
<div className="mt-4 rounded-lg border border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-[12px] text-yellow-600 dark:text-yellow-400">
{t("onboarding.connectorWarning")}
</div>
) : null}
@@ -24,16 +24,16 @@ export function LanguageStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[8px] p-7"
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
<GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.selectLanguage")} / Select Language
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-2.5">
{APP_LOCALES.map((locale) => (
@@ -41,21 +41,21 @@ export function LanguageStep({
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-[8px] border px-4 py-3.5 text-left transition ${
className={`flex w-full items-center gap-3 rounded-lg border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
? "border-[color:var(--primary)] bg-accent"
: "border-border bg-muted hover:border-[color:var(--muted-foreground)]"
}`}
>
<div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
: "border-[color:var(--line)]"
? "border-[color:var(--primary)] bg-primary"
: "border-border"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-[color:var(--text-on-accent)]" /> : null}
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-primary-foreground" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
currentLocale === locale ? "text-foreground" : "text-muted-foreground"
}`}>
{LOCALE_LABELS[locale]}
</span>
@@ -26,53 +26,53 @@ export function LaunchStep({
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[8px] p-7"
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[color:var(--success-soft)] text-[color:var(--success)]">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400">
<CheckCircle size={24} weight="fill" />
</div>
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-foreground">
{launchSummary.companyName} {t("onboarding.isLive")}
</h2>
<p className="mt-3 text-[14px] leading-relaxed text-[color:var(--muted)]">
<p className="mt-3 text-[14px] leading-relaxed text-muted-foreground">
{launchSummary.runId ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
</p>
<div className="mt-6 grid gap-3 md:grid-cols-2">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-foreground">
{selectedConnector?.label ?? t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
</div>
</div>
</div>
{error ? (
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive">
{error}
</div>
) : null}
@@ -32,15 +32,15 @@ export function MissionStep({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[8px] p-7"
className="bg-card border border-border shadow-sm w-full rounded-lg p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("onboarding.stepLabel2")} 2 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-foreground">
{t("onboarding.step2Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="mb-6 h-px bg-[color:var(--border)]" />
<div className="space-y-4">
<Input
@@ -56,8 +56,8 @@ export function MissionStep({
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
rows={5}
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] text-[color:var(--muted)]">
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3 text-[12px] text-muted-foreground">
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground">
<Cpu size={12} />
{t("onboarding.executionHandoff")}
</div>
@@ -70,7 +70,7 @@ export function MissionStep({
</div>
{error ? (
<div className="mt-5 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-5 rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive">
{error}
</div>
) : null}
@@ -79,7 +79,7 @@ export function MissionStep({
<button
type="button"
onClick={onBack}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="text-[12px] font-medium text-muted-foreground transition hover:text-foreground"
>
{t("onboarding.back")}
</button>
@@ -47,13 +47,13 @@ export function OnboardingHero({
<div
className="pointer-events-none absolute inset-0 opacity-[0.035]"
style={{
background: "radial-gradient(ellipse 80% 60% at 30% 20%, var(--accent) 0%, transparent 70%)",
background: "radial-gradient(ellipse 80% 60% at 30% 20%, var(--primary) 0%, transparent 70%)",
}}
/>
<div
className="pointer-events-none absolute inset-0 opacity-[0.025]"
style={{
background: "radial-gradient(ellipse 60% 80% at 70% 80%, var(--accent) 0%, transparent 70%)",
background: "radial-gradient(ellipse 60% 80% at 70% 80%, var(--primary) 0%, transparent 70%)",
animation: "hero-drift 12s ease-in-out infinite alternate",
}}
/>
@@ -77,10 +77,10 @@ export function OnboardingHero({
<div
className="h-px flex-1 max-w-[32px]"
style={{
background: "linear-gradient(90deg, var(--accent), transparent)",
background: "linear-gradient(90deg, var(--primary), transparent)",
}}
/>
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[color:var(--muted)]">
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
{t("onboarding.subtitle")}
</span>
</motion.div>
@@ -90,7 +90,7 @@ export function OnboardingHero({
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15, ease: [0.16, 1, 0.3, 1] }}
className="text-[36px] font-bold leading-[1.05] tracking-[-0.03em] text-[color:var(--text)] md:text-[44px]"
className="text-[36px] font-bold leading-[1.05] tracking-[-0.03em] text-foreground md:text-[44px]"
>
{t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
@@ -102,7 +102,7 @@ export function OnboardingHero({
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.25, ease: [0.16, 1, 0.3, 1] }}
className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]"
className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-muted-foreground"
>
{t("onboarding.description")}
</motion.p>
@@ -118,15 +118,15 @@ export function OnboardingHero({
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 transition-colors duration-300 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
<div className={`h-px w-6 transition-colors duration-300 ${index <= stepIndex ? "bg-primary" : "bg-[color:var(--border)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium transition-colors duration-300 ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
? "bg-accent text-primary"
: "bg-muted text-muted-foreground"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
@@ -156,13 +156,13 @@ export function OnboardingHero({
<div
className="h-px w-8"
style={{
background: "linear-gradient(90deg, var(--accent), transparent)",
background: "linear-gradient(90deg, var(--primary), transparent)",
}}
/>
<span
className="select-none text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{
background: "linear-gradient(135deg, var(--accent) 0%, var(--muted) 100%)",
background: "linear-gradient(135deg, var(--primary) 0%, var(--muted-foreground) 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
@@ -187,7 +187,7 @@ export function CompanyOnboarding({
}, [launchSummary, onDone, t]);
return (
<div className="relative grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
<div className="relative grid min-h-[100dvh] overflow-hidden bg-background md:grid-cols-[minmax(0,1fr)_540px]">
{/* Drag region for macOS frameless title bar */}
<div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" />
<OnboardingHero
@@ -199,13 +199,13 @@ export function CompanyOnboarding({
t={t}
/>
<div className="min-h-0 overflow-y-auto border-l border-[color:var(--line)] bg-[color:var(--bg-subtle)]">
<div className="min-h-0 overflow-y-auto border-l border-border bg-background">
<div className="relative flex min-h-full items-center px-6 py-10 md:px-10">
{canCancel && onCancel ? (
<button
type="button"
onClick={onCancel}
className="absolute right-6 top-6 text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="absolute right-6 top-6 text-[12px] font-medium text-muted-foreground transition hover:text-foreground"
>
{t("onboarding.cancel")}
</button>
+2 -2
View File
@@ -14,8 +14,8 @@ export function EntityWorkspace({
return (
<div className="space-y-14">
<div>
{eyebrow ? <div className="text-[10px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{eyebrow}</div> : null}
<h2 className={`${eyebrow ? "mt-2" : ""} text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]`}>{title}</h2>
{eyebrow ? <div className="text-[10px] uppercase tracking-[0.08em] text-muted-foreground">{eyebrow}</div> : null}
<h2 className={`${eyebrow ? "mt-2" : ""} text-[18px] font-semibold tracking-[-0.01em] text-foreground`}>{title}</h2>
</div>
<div className="space-y-14">{primary}{secondary}</div>
</div>
+4 -4
View File
@@ -4,9 +4,9 @@ import { useAppStore } from "../state/store"
import { useReducedMotion } from "./ui"
const TONE_CONFIG = {
success: { icon: CheckCircle, bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" },
danger: { icon: XCircle, bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" },
warn: { icon: Warning, bg: "var(--warn-soft)", border: "var(--warn)", text: "var(--warn)" },
success: { icon: CheckCircle, bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" },
danger: { icon: XCircle, bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" },
warn: { icon: Warning, bg: "var(--muted-foreground)", border: "var(--destructive)", text: "var(--destructive)" },
} as const
export default function ToastContainer() {
@@ -29,7 +29,7 @@ export default function ToastContainer() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.96 }}
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
className="flex w-[320px] items-start gap-2.5 rounded-[8px] border px-4 py-3 shadow-lg"
className="flex w-[320px] items-start gap-2.5 rounded-lg border px-4 py-3 shadow-lg"
style={{ backgroundColor: cfg.bg, borderColor: cfg.border }}
>
<Icon size={16} weight="fill" className="mt-px shrink-0" style={{ color: cfg.text }} />
@@ -15,21 +15,21 @@ function DeleteConfirmDialog({ open, entityName, onConfirm, onCancel }: { open:
if (!open) return null
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onCancel}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-[8px] border bg-[color:var(--panel)] p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-[color:var(--danger-soft)]"><Trash size={16} className="text-[color:var(--danger)]" /></div><h3 className="text-[15px] font-bold text-[color:var(--text)]">Delete</h3></div>
<p className="mt-2 text-[13px] leading-relaxed text-[color:var(--muted)]">Are you sure you want to delete <strong className="text-[color:var(--text)]">{entityName}</strong>? This action cannot be undone.</p>
<div className="mt-5 flex justify-end gap-3"><button type="button" onClick={onCancel} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">Cancel</button><ActionButton label="Delete" onClick={onConfirm} icon={Trash} tone="danger" /></div>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.15 }} className="w-full max-w-[380px] rounded-lg border bg-card p-6 shadow-xl" style={{ borderColor: LINE }} onClick={(e) => e.stopPropagation()}>
<div className="mb-1 flex items-center gap-2"><div className="flex h-8 w-8 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50"><Trash size={16} className="text-destructive" /></div><h3 className="text-[15px] font-bold text-foreground">Delete</h3></div>
<p className="mt-2 text-[13px] leading-relaxed text-muted-foreground">Are you sure you want to delete <strong className="text-foreground">{entityName}</strong>? This action cannot be undone.</p>
<div className="mt-5 flex justify-end gap-3"><button type="button" onClick={onCancel} className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted">Cancel</button><ActionButton label="Delete" onClick={onConfirm} icon={Trash} tone="danger" /></div>
</motion.div>
</motion.div>
)
}
function AutomationActivityLog({ entries }: { entries: AutomationLogEntry[] }) {
if (entries.length === 0) return <div className="py-6 text-center text-[13px] text-[color:var(--muted)]">No activity yet</div>
if (entries.length === 0) return <div className="py-6 text-center text-[13px] text-muted-foreground">No activity yet</div>
return (
<div className="space-y-1">{entries.map((entry, idx) => {
const ti = triggerConfig[entry.trigger as AutomationTrigger]; const ai = actionConfig[entry.action as AutomationAction]; const TIcon = ti?.icon ?? Lightning
return <motion.div key={`${entry.ruleId}-${entry.executedAt}-${idx}`} initial={{ opacity: 0, x: -6 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: idx * 0.03 }} className="flex items-center gap-3 rounded-[8px] border bg-[color:var(--panel)] px-4 py-2.5 text-[12px]" style={{ borderColor: LINE }}><TIcon size={14} style={{ color: ACCENT }} /><div className="min-w-0 flex-1"><div className="flex items-center gap-2"><span className="truncate font-medium text-[color:var(--text)]">{entry.ruleName}</span></div><div className="mt-0.5 flex items-center gap-1.5 text-[10px] text-[color:var(--muted)]"><span>{ti?.label ?? entry.trigger}</span><ArrowRight size={8} /><span>{ai?.label ?? entry.action}</span></div></div><span className="shrink-0 text-[10px] text-[color:var(--muted)]">{timeAgo(entry.executedAt)}</span></motion.div>
return <motion.div key={`${entry.ruleId}-${entry.executedAt}-${idx}`} initial={{ opacity: 0, x: -6 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: idx * 0.03 }} className="flex items-center gap-3 rounded-lg border bg-card px-4 py-2.5 text-[12px]" style={{ borderColor: LINE }}><TIcon size={14} style={{ color: ACCENT }} /><div className="min-w-0 flex-1"><div className="flex items-center gap-2"><span className="truncate font-medium text-foreground">{entry.ruleName}</span></div><div className="mt-0.5 flex items-center gap-1.5 text-[10px] text-muted-foreground"><span>{ti?.label ?? entry.trigger}</span><ArrowRight size={8} /><span>{ai?.label ?? entry.action}</span></div></div><span className="shrink-0 text-[10px] text-muted-foreground">{timeAgo(entry.executedAt)}</span></motion.div>
})}</div>
)
}
@@ -47,14 +47,14 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Automation Rules</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--success)" }].map((s) => <div key={s.l} className="rounded-[8px] border bg-[color:var(--panel)] px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}>Activity Log ({actLog.length})</button></div>
{actionError ? <div className="flex items-center justify-between rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Automation Rules</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{rules.length > 0 ? <div className="grid grid-cols-3 gap-2">{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--primary)" }].map((s) => <div key={s.l} className="rounded-lg border bg-card px-4 py-3.5" style={{ borderColor: LINE }}><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{s.l}</div><div className="text-[26px] font-bold tabular-nums" style={s.c ? { color: s.c } : undefined}>{s.v}</div></div>)}</div> : null}
<div className="flex gap-1 rounded-lg bg-muted p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Activity Log ({actLog.length})</button></div>
{activeTab === "rules" ? <>
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
: filtered.length === 0 ? <div className="py-8 text-center text-[13px] text-[color:var(--muted)]">No rules match the current filter.</div>
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
: filtered.length === 0 ? <div className="py-8 text-center text-[13px] text-muted-foreground">No rules match the current filter.</div>
: <div className="space-y-2"><AnimatePresence>{filtered.map((r) => <RuleCard key={r.id} rule={r} onToggle={() => void handleToggle(r)} onDelete={() => setDelTarget(r)} onEdit={() => setEditRule(r)} />)}</AnimatePresence></div>}
</> : <AutomationActivityLog entries={actLog} />}
<AnimatePresence>{showCreate ? <RuleFormModal open={showCreate} editingRule={null} companyId={companyId} onClose={() => setShowCreate(false)} onSaved={onRefresh} /> : null}{editRule ? <RuleFormModal open={!!editRule} editingRule={editRule} companyId={companyId} onClose={() => setEditRule(null)} onSaved={onRefresh} /> : null}</AnimatePresence>
@@ -71,9 +71,9 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-[color:var(--danger)] hover:text-[color:var(--text-on-accent)]"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-[color:var(--text)]">Workflow Pipelines</h2><p className="text-[12px] text-[color:var(--muted)]">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-[color:var(--muted)]" /><div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-[color:var(--muted)]">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
{actionError ? <div className="flex items-center justify-between rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Workflow Pipelines</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
: <div className="space-y-2"><AnimatePresence>{workflows.map((wf) => <WorkflowCard key={wf.id} workflow={wf} isRunning={runningId === wf.id} onDelete={() => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}</AnimatePresence></div>}
<AnimatePresence>{showCreate ? <WorkflowFormModal open={showCreate} editingWorkflow={null} companyId={companyId} onClose={() => setShowCreate(false)} onSaved={onRefresh} /> : null}{editWf ? <WorkflowFormModal open={!!editWf} editingWorkflow={editWf} companyId={companyId} onClose={() => setEditWf(null)} onSaved={onRefresh} /> : null}</AnimatePresence>
<AnimatePresence>{delTarget ? <DeleteConfirmDialog open={!!delTarget} entityName={delTarget.name} onConfirm={() => void handleDelete()} onCancel={() => setDelTarget(null)} /> : null}</AnimatePresence>
@@ -85,10 +85,10 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
const t = useT(); const [activeSection, setActiveSection] = useState<"rules"|"workflows">("rules")
return (
<div className="space-y-6">
<div className="rounded-[8px] border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1">
<button type="button" onClick={() => setActiveSection("rules")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} style={activeSection === "rules" ? { color: ACCENT } : undefined} />Automation Rules ({rules.length})</button>
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-[8px] px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} style={activeSection === "workflows" ? { color: ACCENT } : undefined} />Workflows ({workflows.length})</button>
<div className="rounded-lg border px-5 py-4" style={{ borderColor: `${ACCENT}30`, background: `linear-gradient(135deg, ${ACCENT}08 0%, ${ACCENT}04 100%)` }}><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}18`, color: ACCENT }}><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-foreground">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
<div className="flex gap-1 rounded-lg bg-muted p-1">
<button type="button" onClick={() => setActiveSection("rules")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}><Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} style={activeSection === "rules" ? { color: ACCENT } : undefined} />Automation Rules ({rules.length})</button>
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} style={activeSection === "workflows" ? { color: ACCENT } : undefined} />Workflows ({workflows.length})</button>
</div>
<AnimatePresence mode="wait"><motion.div key={activeSection} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}>{activeSection === "rules" ? <RulesPanel rules={rules} companyId={companyId} onRefresh={onRefresh} /> : <WorkflowsPanel workflows={workflows} companyId={companyId} onRefresh={onRefresh} />}</motion.div></AnimatePresence>
</div>
+11 -11
View File
@@ -9,25 +9,25 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
const TriggerIcon = triggerInfo?.icon ?? Lightning; const colors = statusColors[rule.status]
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition duration-150 hover:shadow-md hover:-translate-y-px" style={{ borderColor: LINE }}>
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-lg border bg-card transition duration-150 hover:shadow-md hover:-translate-y-px" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={onEdit} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{rule.name}</span>
<span className="truncate text-[13px] font-bold text-foreground">{rule.name}</span>
<span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} aria-hidden="true" />{rule.status}</span>
</div>
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[color:var(--muted)]">
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? rule.trigger}</span>
<ArrowRight size={10} className="text-[color:var(--muted)]" />
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Gear size={10} />{actionInfo?.label ?? rule.action}</span>
{rule.sourceDepartment || rule.targetDepartment ? <><span className="text-[color:var(--line-strong)]">|</span><span className="inline-flex items-center gap-1"><span className="font-medium text-[color:var(--muted-strong)]">{rule.sourceDepartment ?? "Any"}</span><CaretRight size={8} /><span className="font-medium text-[color:var(--muted-strong)]">{rule.targetDepartment ?? "Same"}</span></span></> : null}
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1 rounded-[4px] bg-muted px-1.5 py-0.5 font-medium text-muted-foreground"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? rule.trigger}</span>
<ArrowRight size={10} className="text-muted-foreground" />
<span className="inline-flex items-center gap-1 rounded-[4px] bg-muted px-1.5 py-0.5 font-medium text-muted-foreground"><Gear size={10} />{actionInfo?.label ?? rule.action}</span>
{rule.sourceDepartment || rule.targetDepartment ? <><span className="text-[color:var(--border)]">|</span><span className="inline-flex items-center gap-1"><span className="font-medium text-muted-foreground">{rule.sourceDepartment ?? "Any"}</span><CaretRight size={8} /><span className="font-medium text-muted-foreground">{rule.targetDepartment ?? "Same"}</span></span></> : null}
</div>
<div className="mt-1 flex items-center gap-3 text-[10px] text-[color:var(--muted)]"><span className="tabular-nums">{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span className="tabular-nums">Priority {rule.priority}</span></div>
<div className="mt-1 flex items-center gap-3 text-[10px] text-muted-foreground"><span className="tabular-nums">{rule.executionCount} execution{rule.executionCount !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>Last run {timeAgo(rule.lastExecutedAt)}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span className="tabular-nums">Priority {rule.priority}</span></div>
</button>
<div className="flex shrink-0 items-center gap-1">
<button type="button" onClick={onToggle} aria-label={rule.status === "active" ? "Pause rule" : "Activate rule"} title={rule.status === "active" ? "Pause rule" : "Activate rule"} className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]">{rule.status === "active" ? <Pause size={15} /> : <Play size={15} />}</button>
<button type="button" onClick={onDelete} aria-label="Delete rule" title="Delete rule" className="rounded-[6px] p-2 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100"><Trash size={15} /></button>
<button type="button" onClick={onToggle} aria-label={rule.status === "active" ? "Pause rule" : "Activate rule"} title={rule.status === "active" ? "Pause rule" : "Activate rule"} className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground">{rule.status === "active" ? <Pause size={15} /> : <Play size={15} />}</button>
<button type="button" onClick={onDelete} aria-label="Delete rule" title="Delete rule" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-red-100 dark:bg-red-900/50 hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
</div>
</div>
</motion.div>
@@ -25,24 +25,24 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved }
const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action]
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[580px] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Automation Rule" : "New Automation Rule"}</h2></div>
<button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button>
<div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-foreground">{isEditing ? "Edit Automation Rule" : "New Automation Rule"}</h2></div>
<button type="button" onClick={onClose} className="rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground" aria-label="Close"><X size={16} /></button>
</div>
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
{error ? <div className="rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]">{error}</div> : null}
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Rule Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Auto-assign blocked tasks" className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="What does this rule do?" rows={2} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Trigger</label><Select value={form.trigger} onChange={(v) => updateField("trigger", v as AutomationTrigger)} options={triggerOptions} />{selectedTriggerInfo ? <div className="mt-1.5 text-[11px] text-[color:var(--muted)]">{selectedTriggerInfo.description}</div> : null}</div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Conditions (JSON)</label><textarea value={form.conditionsJson} onChange={(e) => updateField("conditionsJson", e.target.value)} rows={3} spellCheck={false} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 font-mono text-[12px] text-[color:var(--text)]" style={{ borderColor: LINE }} placeholder='{"status": "blocked"}' /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Action</label><Select value={form.action} onChange={(v) => updateField("action", v as AutomationAction)} options={actionOptions} />{selectedActionInfo ? <div className="mt-1.5 text-[11px] text-[color:var(--muted)]">{selectedActionInfo.description}</div> : null}</div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Action Configuration (JSON)</label><textarea value={form.actionConfigJson} onChange={(e) => updateField("actionConfigJson", e.target.value)} rows={3} spellCheck={false} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 font-mono text-[12px] text-[color:var(--text)]" style={{ borderColor: LINE }} placeholder='{"targetAgentId": "..."}' /></div>
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Source Department</label><Select value={form.sourceDepartment} onChange={(v) => updateField("sourceDepartment", v)} options={departmentOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Target Department</label><Select value={form.targetDepartment} onChange={(v) => updateField("targetDepartment", v)} options={departmentOptions} /></div></div>
<div><div className="mb-1.5 flex items-center justify-between"><label className="text-[12px] font-medium text-[color:var(--muted-strong)]">Priority</label><span className="text-[12px] font-semibold" style={{ color: ACCENT }}>{form.priority}</span></div><input type="range" min={0} max={100} value={form.priority} onChange={(e) => updateField("priority", parseInt(e.target.value, 10))} className="w-full accent-[#d97745]" /><div className="mt-1 flex justify-between text-[10px] text-[color:var(--muted)]"><span>Low priority</span><span>High priority</span></div></div>
{error ? <div className="rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Rule Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Auto-assign blocked tasks" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="What does this rule do?" rows={2} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Trigger</label><Select value={form.trigger} onChange={(v) => updateField("trigger", v as AutomationTrigger)} options={triggerOptions} />{selectedTriggerInfo ? <div className="mt-1.5 text-[11px] text-muted-foreground">{selectedTriggerInfo.description}</div> : null}</div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Conditions (JSON)</label><textarea value={form.conditionsJson} onChange={(e) => updateField("conditionsJson", e.target.value)} rows={3} spellCheck={false} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 font-mono text-[12px] text-foreground" style={{ borderColor: LINE }} placeholder='{"status": "blocked"}' /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Action</label><Select value={form.action} onChange={(v) => updateField("action", v as AutomationAction)} options={actionOptions} />{selectedActionInfo ? <div className="mt-1.5 text-[11px] text-muted-foreground">{selectedActionInfo.description}</div> : null}</div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Action Configuration (JSON)</label><textarea value={form.actionConfigJson} onChange={(e) => updateField("actionConfigJson", e.target.value)} rows={3} spellCheck={false} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 font-mono text-[12px] text-foreground" style={{ borderColor: LINE }} placeholder='{"targetAgentId": "..."}' /></div>
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Source Department</label><Select value={form.sourceDepartment} onChange={(v) => updateField("sourceDepartment", v)} options={departmentOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Target Department</label><Select value={form.targetDepartment} onChange={(v) => updateField("targetDepartment", v)} options={departmentOptions} /></div></div>
<div><div className="mb-1.5 flex items-center justify-between"><label className="text-[12px] font-medium text-muted-foreground">Priority</label><span className="text-[12px] font-semibold" style={{ color: ACCENT }}>{form.priority}</span></div><input type="range" min={0} max={100} value={form.priority} onChange={(e) => updateField("priority", parseInt(e.target.value, 10))} className="w-full accent-[#d97745]" /><div className="mt-1 flex justify-between text-[10px] text-muted-foreground"><span>Low priority</span><span>High priority</span></div></div>
</div></div>
<div className="flex shrink-0 justify-end gap-3 border-t px-6 py-4" style={{ borderColor: LINE }}>
<button type="button" onClick={onClose} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]">Cancel</button>
<button type="button" onClick={onClose} className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground">Cancel</button>
<ActionButton label={isEditing ? "Save Changes" : "Create Rule"} onClick={() => void handleSave()} icon={isEditing ? Gear : Lightning} tone="accent" loading={saving} disabled={!form.name.trim()} />
</div>
</motion.div>
@@ -25,18 +25,18 @@ export function WorkflowFormModal({ open, editingWorkflow, companyId, onClose, o
const statusOptions = [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "paused", label: "Paused" }]
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-[8px] border bg-[color:var(--panel)] shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}><div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><ArrowsClockwise size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-[color:var(--text)]">{isEditing ? "Edit Workflow" : "New Workflow"}</h2></div><button type="button" onClick={onClose} className="rounded-[6px] p-1.5 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" aria-label="Close"><X size={16} /></button></div>
<motion.div initial={{ scale: 0.95, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.95, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="flex w-full max-w-[540px] flex-col rounded-lg border bg-card shadow-xl" style={{ borderColor: LINE, maxHeight: "85vh" }} onClick={(e) => e.stopPropagation()}>
<div className="flex shrink-0 items-center justify-between border-b px-6 py-4" style={{ borderColor: LINE }}><div className="flex items-center gap-3"><div className="flex h-8 w-8 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><ArrowsClockwise size={18} weight="fill" /></div><h2 className="text-[16px] font-bold text-foreground">{isEditing ? "Edit Workflow" : "New Workflow"}</h2></div><button type="button" onClick={onClose} className="rounded-lg p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground" aria-label="Close"><X size={16} /></button></div>
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5">
{error ? <div className="rounded-[8px] bg-[color:var(--danger-soft)] px-3.5 py-2.5 text-[12px] font-medium text-[color:var(--danger)]">{error}</div> : null}
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Workflow Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Bug Fix Pipeline" className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="Describe this workflow pipeline" rows={2} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" style={{ borderColor: LINE }} /></div>
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Trigger</label><Select value={form.triggerType} onChange={(v) => updateField("triggerType", v as AutomationTrigger)} options={triggerOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Status</label><Select value={form.status} onChange={(v) => updateField("status", v as WorkflowStatus)} options={statusOptions} /></div></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Trigger Configuration (JSON)</label><textarea value={form.triggerConfigJson} onChange={(e) => updateField("triggerConfigJson", e.target.value)} rows={2} spellCheck={false} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 font-mono text-[12px] text-[color:var(--text)]" style={{ borderColor: LINE }} placeholder="{}" /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Pipeline Steps (JSON Array)</label><textarea value={form.stepsJson} onChange={(e) => updateField("stepsJson", e.target.value)} rows={6} spellCheck={false} className="focus-ring w-full rounded-[8px] border bg-[color:var(--panel)] px-3.5 py-2.5 font-mono text-[12px] text-[color:var(--text)]" style={{ borderColor: LINE }} placeholder='[{"id":"step-1","name":"Assign","action":"assign_task","configJson":"{}","assigneeDepartment":"engineering","assigneeAgentId":null,"requiresApproval":false,"timeoutSec":0,"status":"pending"}]' /></div>
{error ? <div className="rounded-lg bg-red-100 dark:bg-red-900/50 px-3.5 py-2.5 text-[12px] font-medium text-destructive">{error}</div> : null}
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Workflow Name</label><input type="text" value={form.name} onChange={(e) => updateField("name", e.target.value)} placeholder="e.g., Bug Fix Pipeline" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Description</label><textarea value={form.description} onChange={(e) => updateField("description", e.target.value)} placeholder="Describe this workflow pipeline" rows={2} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} /></div>
<div className="grid grid-cols-2 gap-3"><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Trigger</label><Select value={form.triggerType} onChange={(v) => updateField("triggerType", v as AutomationTrigger)} options={triggerOptions} /></div><div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Status</label><Select value={form.status} onChange={(v) => updateField("status", v as WorkflowStatus)} options={statusOptions} /></div></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Trigger Configuration (JSON)</label><textarea value={form.triggerConfigJson} onChange={(e) => updateField("triggerConfigJson", e.target.value)} rows={2} spellCheck={false} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 font-mono text-[12px] text-foreground" style={{ borderColor: LINE }} placeholder="{}" /></div>
<div><label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Pipeline Steps (JSON Array)</label><textarea value={form.stepsJson} onChange={(e) => updateField("stepsJson", e.target.value)} rows={6} spellCheck={false} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 font-mono text-[12px] text-foreground" style={{ borderColor: LINE }} placeholder='[{"id":"step-1","name":"Assign","action":"assign_task","configJson":"{}","assigneeDepartment":"engineering","assigneeAgentId":null,"requiresApproval":false,"timeoutSec":0,"status":"pending"}]' /></div>
</div></div>
<div className="flex shrink-0 justify-end gap-3 border-t px-6 py-4" style={{ borderColor: LINE }}>
<button type="button" onClick={onClose} className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]">Cancel</button>
<button type="button" onClick={onClose} className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted hover:text-foreground">Cancel</button>
<ActionButton label={isEditing ? "Save Changes" : "Create Workflow"} onClick={() => void handleSave()} icon={isEditing ? Gear : ArrowsClockwise} tone="accent" loading={saving} disabled={!form.name.trim()} />
</div>
</motion.div>
@@ -7,19 +7,19 @@ import { ActionButton } from "../ui"
import { ACCENT, LINE, triggerConfig, actionConfig, workflowStatusColors, stepStatusConfig, safeJsonParse } from "./automation-config"
function StepTimeline({ steps }: { steps: WorkflowStep[] }) {
if (steps.length === 0) return <div className="py-4 text-center text-[11px] text-[color:var(--muted)]">No steps defined</div>
if (steps.length === 0) return <div className="py-4 text-center text-[11px] text-muted-foreground">No steps defined</div>
return (
<div className="relative pl-5">
<div className="absolute left-[9px] top-2 w-px" style={{ height: `calc(100% - 16px)`, backgroundColor: "var(--line)" }} />
<div className="absolute left-[9px] top-2 w-px" style={{ height: `calc(100% - 16px)`, backgroundColor: "var(--border)" }} />
<div className="space-y-3">
{steps.map((step, idx) => {
const colors = stepStatusConfig[step.status]; const info = actionConfig[step.action]
return (
<div key={step.id} className="relative flex items-start gap-3">
<div className="absolute -left-5 top-1.5 z-10 h-[10px] w-[10px] rounded-full border-2" style={{ backgroundColor: colors.bg, borderColor: colors.border }} />
<div className="flex-1 rounded-[8px] border px-3 py-2.5" style={{ borderColor: step.status === "running" ? ACCENT : LINE }}>
<div className="flex items-center justify-between"><div className="flex items-center gap-2"><span className="text-[11px] font-bold" style={{ color: colors.text }}>{idx + 1}</span><span className="text-[12px] font-medium text-[color:var(--text)]">{step.name}</span></div><span className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}>{step.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-[color:var(--muted)]"><span>{info?.label ?? step.action}</span>{step.assigneeDepartment ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{step.assigneeDepartment}</span></> : null}{step.requiresApproval ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span className="text-[color:var(--warn)]">Requires approval</span></> : null}{step.timeoutSec > 0 ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{step.timeoutSec}s timeout</span></> : null}</div>
<div className="flex-1 rounded-lg border px-3 py-2.5" style={{ borderColor: step.status === "running" ? ACCENT : LINE }}>
<div className="flex items-center justify-between"><div className="flex items-center gap-2"><span className="text-[11px] font-bold" style={{ color: colors.text }}>{idx + 1}</span><span className="text-[12px] font-medium text-foreground">{step.name}</span></div><span className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}>{step.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground"><span>{info?.label ?? step.action}</span>{step.assigneeDepartment ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.assigneeDepartment}</span></> : null}{step.requiresApproval ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span className="text-yellow-600 dark:text-yellow-400">Requires approval</span></> : null}{step.timeoutSec > 0 ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.timeoutSec}s timeout</span></> : null}</div>
</div>
</div>
)
@@ -35,22 +35,22 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
const steps: WorkflowStep[] = useMemo(() => { const parsed = safeJsonParse(workflow.stepsJson); return Array.isArray(parsed) ? (parsed as WorkflowStep[]) : [] }, [workflow.stepsJson])
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-[8px] border bg-[color:var(--panel)] transition hover:shadow-sm" style={{ borderColor: LINE }}>
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-lg border bg-card transition hover:shadow-sm" style={{ borderColor: LINE }}>
<div className="flex items-center gap-3 px-4 py-3.5">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px]" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={() => setExpanded((p) => !p)} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2"><span className="truncate text-[13px] font-semibold text-[color:var(--text)]">{workflow.name}</span><span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{workflow.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-[color:var(--muted)]"><span className="inline-flex items-center gap-1 rounded-[4px] bg-[color:var(--panel-soft)] px-1.5 py-0.5 font-medium text-[color:var(--muted-strong)]"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? workflow.triggerType}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{steps.length} step{steps.length !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>{workflow.runCount} run{workflow.runCount !== 1 ? "s" : ""}</span>{workflow.lastRunAt ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted)]" /><span>Last run {timeAgo(workflow.lastRunAt)}</span></> : null}</div>
<div className="flex items-center gap-2"><span className="truncate text-[13px] font-semibold text-foreground">{workflow.name}</span><span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{workflow.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-muted-foreground"><span className="inline-flex items-center gap-1 rounded-[4px] bg-muted px-1.5 py-0.5 font-medium text-muted-foreground"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? workflow.triggerType}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{steps.length} step{steps.length !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{workflow.runCount} run{workflow.runCount !== 1 ? "s" : ""}</span>{workflow.lastRunAt ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>Last run {timeAgo(workflow.lastRunAt)}</span></> : null}</div>
</button>
<div className="flex shrink-0 items-center gap-1">
<motion.div animate={{ rotate: expanded ? 90 : 0 }} transition={{ duration: 0.15 }}><button type="button" onClick={() => setExpanded((p) => !p)} aria-label={expanded ? "Collapse" : "Expand"} className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"><CaretRight size={14} /></button></motion.div>
<button type="button" onClick={onRun} aria-label="Run workflow" disabled={isRunning} className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--success-soft)] hover:text-[color:var(--success)] disabled:pointer-events-none disabled:opacity-50">{isRunning ? <ArrowsClockwise size={15} className="animate-spin" /> : <Play size={15} />}</button>
<button type="button" onClick={onEdit} aria-label="Edit workflow" className="rounded-[6px] p-2 text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"><Gear size={15} /></button>
<button type="button" onClick={onDelete} aria-label="Delete workflow" className="rounded-[6px] p-2 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100"><Trash size={15} /></button>
<motion.div animate={{ rotate: expanded ? 90 : 0 }} transition={{ duration: 0.15 }}><button type="button" onClick={() => setExpanded((p) => !p)} aria-label={expanded ? "Collapse" : "Expand"} className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground"><CaretRight size={14} /></button></motion.div>
<button type="button" onClick={onRun} aria-label="Run workflow" disabled={isRunning} className="rounded-lg p-2 text-muted-foreground transition hover:bg-green-100 dark:bg-green-900/50 hover:text-green-600 dark:text-green-400 disabled:pointer-events-none disabled:opacity-50">{isRunning ? <ArrowsClockwise size={15} className="animate-spin" /> : <Play size={15} />}</button>
<button type="button" onClick={onEdit} aria-label="Edit workflow" className="rounded-lg p-2 text-muted-foreground transition hover:bg-muted hover:text-foreground"><Gear size={15} /></button>
<button type="button" onClick={onDelete} aria-label="Delete workflow" className="rounded-lg p-2 text-muted-foreground opacity-0 transition hover:bg-red-100 dark:bg-red-900/50 hover:text-destructive group-hover:opacity-100"><Trash size={15} /></button>
</div>
</div>
<AnimatePresence>
{expanded ? <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="overflow-hidden"><div className="border-t px-4 py-4" style={{ borderColor: LINE }}>{workflow.description ? <p className="mb-3 text-[12px] leading-relaxed text-[color:var(--muted)]">{workflow.description}</p> : null}<StepTimeline steps={steps} /></div></motion.div> : null}
{expanded ? <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="overflow-hidden"><div className="border-t px-4 py-4" style={{ borderColor: LINE }}>{workflow.description ? <p className="mb-3 text-[12px] leading-relaxed text-muted-foreground">{workflow.description}</p> : null}<StepTimeline steps={steps} /></div></motion.div> : null}
</AnimatePresence>
</motion.div>
)
@@ -2,8 +2,8 @@ import type { AutomationAction, AutomationStatus, AutomationTrigger, WorkflowSta
import { ArrowsClockwise, Eye, Funnel, Lightning, Pause, Play, Plus, Robot, Timer, Trash } from "@phosphor-icons/react"
import { DEPARTMENT_OPTIONS } from "@shared/constants"
export const ACCENT = "var(--accent)"
export const LINE = "var(--line)"
export const ACCENT = "var(--primary)"
export const LINE = "var(--border)"
export const triggerConfig: Record<AutomationTrigger, { label: string; description: string; icon: typeof Lightning }> = {
task_status_changed: { label: "Task Status Changed", description: "When a task transitions to a new status", icon: ArrowsClockwise },
@@ -44,26 +44,26 @@ export const actionConfig: Record<AutomationAction, { label: string; description
export const departmentOptions = DEPARTMENT_OPTIONS
export const statusColors: Record<AutomationStatus, { bg: string; text: string; dot: string }> = {
active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" },
paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" },
disabled: { bg: "var(--panel-soft)", text: "var(--muted)", dot: "var(--muted)" },
error: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" },
active: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
paused: { bg: "var(--muted-foreground)", text: "var(--destructive)", dot: "var(--destructive)" },
disabled: { bg: "var(--muted-foreground)", text: "var(--muted-foreground)", dot: "var(--muted-foreground)" },
error: { bg: "var(--destructive)", text: "var(--destructive)", dot: "var(--destructive)" },
}
export const workflowStatusColors: Record<WorkflowStatus, { bg: string; text: string; dot: string }> = {
draft: { bg: "var(--panel-soft)", text: "var(--muted-strong)", dot: "var(--muted)" },
active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" },
paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" },
completed: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" },
failed: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" },
draft: { bg: "var(--muted-foreground)", text: "var(--muted-foreground)", dot: "var(--muted-foreground)" },
active: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
paused: { bg: "var(--muted-foreground)", text: "var(--destructive)", dot: "var(--destructive)" },
completed: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
failed: { bg: "var(--destructive)", text: "var(--destructive)", dot: "var(--destructive)" },
}
export const stepStatusConfig: Record<WorkflowStep["status"], { bg: string; border: string; text: string }> = {
pending: { bg: "var(--panel-soft)", border: "var(--line)", text: "var(--muted)" },
running: { bg: "var(--accent-soft)", border: ACCENT, text: ACCENT },
completed: { bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" },
failed: { bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" },
skipped: { bg: "var(--panel-soft)", border: "var(--line)", text: "var(--muted)" },
pending: { bg: "var(--muted-foreground)", border: "var(--border)", text: "var(--muted-foreground)" },
running: { bg: "var(--primary)", border: ACCENT, text: ACCENT },
completed: { bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" },
failed: { bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" },
skipped: { bg: "var(--muted-foreground)", border: "var(--border)", text: "var(--muted-foreground)" },
}
export interface AutomationLogEntry { ruleId: string; ruleName: string; trigger: string; action: string; executedAt: string }
@@ -47,12 +47,12 @@ export function ChannelHeader({
const t = useT()
return (
<div className="flex items-center gap-3 border-b border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5">
<div className="flex items-center gap-3 border-b border-border bg-card px-4 py-2.5">
{!showSidebar ? (
<button
type="button"
onClick={onShowSidebar}
className="rounded-[4px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="rounded-[4px] p-1 text-muted-foreground transition hover:text-foreground"
aria-label="Show sidebar"
>
<CaretDown size={12} className="rotate-90" />
@@ -61,15 +61,15 @@ export function ChannelHeader({
{activeChannel ? (
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-semibold text-[color:var(--text)]">{activeChannel.label}</div>
<div className="truncate text-[13px] font-semibold text-foreground">{activeChannel.label}</div>
{activeChannel.kind === "task" && activeTask ? (
<button type="button" onClick={() => onNavigateToTask(activeTask.id)} className="mt-0.5 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline">
<button type="button" onClick={() => onNavigateToTask(activeTask.id)} className="mt-0.5 text-[11px] font-medium text-primary transition hover:underline">
Open task
</button>
) : null}
</div>
) : (
<span className="text-[13px] text-[color:var(--muted)]">{t("comm.selectChannel")}</span>
<span className="text-[13px] text-muted-foreground">{t("comm.selectChannel")}</span>
)}
<div className="ml-auto w-full max-w-[320px]">
@@ -7,11 +7,11 @@ import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeade
import { useT } from "../../i18n"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--accent)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
company: { label: "Company-wide", icon: Megaphone, color: "var(--accent)" },
project: { label: "Project", icon: Hash, color: "var(--success)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--primary)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-foreground)" },
company: { label: "Company-wide", icon: Megaphone, color: "var(--primary)" },
project: { label: "Project", icon: Hash, color: "var(--primary)" },
incident: { label: "Incident", icon: Fire, color: "var(--destructive)" },
}
function ChannelItem({
@@ -38,15 +38,15 @@ function ChannelItem({
type="button"
onClick={onClick}
aria-current={active ? "page" : undefined}
className={`group relative flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
active ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
className={`group relative flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left transition duration-150 ${
active ? "bg-accent" : "hover:bg-muted/50"
}`}
>
{descriptor.channel === "direct" && agent ? (
<AgentAvatar agent={agent} size="sm" />
) : (
<div
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${config.color}18` }}
>
<Icon size={14} weight={active ? "fill" : "regular"} style={{ color: config.color }} />
@@ -56,21 +56,21 @@ function ChannelItem({
<span
className={`min-w-0 flex-1 truncate text-[13px] ${
active
? "font-semibold text-[color:var(--text)]"
? "font-semibold text-foreground"
: unreadCount > 0
? "font-semibold text-[color:var(--text)]"
: "font-medium text-[color:var(--muted-strong)]"
? "font-semibold text-foreground"
: "font-medium text-muted-foreground"
}`}
>
{descriptor.label}
</span>
{hasUrgent ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-[color:var(--danger)]" />
<span className="h-2 w-2 shrink-0 rounded-full bg-destructive" />
) : unreadCount > 0 ? (
<span
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-[color:var(--text-on-accent)]"
style={{ backgroundColor: "var(--accent)" }}
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-primary-foreground"
style={{ backgroundColor: "var(--primary)" }}
>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
@@ -108,13 +108,13 @@ export function ChannelSidebar({
animate={{ width: 260, opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="flex h-full shrink-0 flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]"
className="flex h-full shrink-0 flex-col overflow-hidden border-r border-border bg-card"
style={{ width: 260 }}
>
<div className="flex-1 overflow-y-auto px-2 py-3">
{channelGroups.map((group) => (
<div key={group.type} className="mb-3">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{group.label}
</div>
{group.items.map((ch) => {
@@ -136,7 +136,7 @@ export function ChannelSidebar({
{taskDiscussionChannels.length > 0 ? (
<div className="mb-3">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<div className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{t("comm.taskDiscussions")}
</div>
{taskDiscussionChannels.map((discussion) => (
@@ -144,14 +144,14 @@ export function ChannelSidebar({
key={discussion.key}
type="button"
onClick={() => onSelectChannel(discussion.key)}
className={`group flex w-full items-center gap-2.5 rounded-[8px] px-3 py-2 text-left transition duration-150 ${
selectedChannelKey === discussion.key ? "bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50"
className={`group flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left transition duration-150 ${
selectedChannelKey === discussion.key ? "bg-muted" : "hover:bg-muted/50"
}`}
>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px]" style={{ backgroundColor: "var(--accent-soft)" }}>
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--accent)" }} />
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: "var(--primary)" }}>
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--primary)" }} />
</div>
<span className={`min-w-0 flex-1 truncate text-[13px] ${selectedChannelKey === discussion.key ? "font-semibold text-[color:var(--text)]" : "font-medium text-[color:var(--muted-strong)]"}`}>
<span className={`min-w-0 flex-1 truncate text-[13px] ${selectedChannelKey === discussion.key ? "font-semibold text-foreground" : "font-medium text-muted-foreground"}`}>
{discussion.label}
</span>
</button>
@@ -160,14 +160,14 @@ export function ChannelSidebar({
) : null}
{totalChannels === 0 ? (
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
<div className="px-3 py-6 text-center text-[11px] text-muted-foreground">
{t("comm.noActiveDepartments")}
</div>
) : null}
</div>
<div className="border-t border-[color:var(--line)] px-4 py-2">
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<div className="border-t border-border px-4 py-2">
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
<span>{totalChannels} channels</span>
<span>{agents.length} agents</span>
</div>
@@ -201,29 +201,29 @@ export function FilterBar({
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="relative min-w-0 flex-1">
<MagnifyingGlass size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[color:var(--muted)]" />
<MagnifyingGlass size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
value={filters.search}
onChange={(e) => onChangeFilters({ ...filters, search: e.target.value })}
onKeyDown={onSearchKeyDown}
placeholder={t("comm.searchMessages")}
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
style={{ focusRingColor: "var(--accent)" } as React.CSSProperties}
className="w-full rounded-lg border border-border bg-card py-1.5 pl-8 pr-3 text-[11px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1"
style={{ focusRingColor: "var(--primary)" } as React.CSSProperties}
/>
</div>
{scopeToggle}
<button
type="button"
onClick={() => setExpanded(!expanded)}
className={`flex items-center gap-1.5 rounded-[6px] border px-2.5 py-1.5 text-[11px] font-medium transition ${
className={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[11px] font-medium transition ${
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? "border-transparent text-[color:var(--text-on-accent)]"
: "border-[color:var(--line)] text-[color:var(--muted-strong)] hover:border-[color:var(--line-strong)]"
? "border-transparent text-primary-foreground"
: "border-border text-muted-foreground hover:border-border"
}`}
style={
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? { backgroundColor: "var(--accent)" }
? { backgroundColor: "var(--primary)" }
: undefined
}
>
@@ -243,7 +243,7 @@ export function FilterBar({
<select
value={filters.channelType}
onChange={(e) => onChangeFilters({ ...filters, channelType: e.target.value as MessageChannel | "all" })}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
className="rounded-lg border border-border bg-card px-2 py-1 text-[11px] font-medium text-foreground"
>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
@@ -255,7 +255,7 @@ export function FilterBar({
<select
value={filters.priority}
onChange={(e) => onChangeFilters({ ...filters, priority: e.target.value })}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]"
className="rounded-lg border border-border bg-card px-2 py-1 text-[11px] font-medium text-foreground"
>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
@@ -266,9 +266,9 @@ export function FilterBar({
type="button"
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${
filters.unreadOnly ? "text-[color:var(--text-on-accent)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
filters.unreadOnly ? "text-primary-foreground" : "bg-muted text-muted-foreground"
}`}
style={filters.unreadOnly ? { backgroundColor: "var(--accent)" } : undefined}
style={filters.unreadOnly ? { backgroundColor: "var(--primary)" } : undefined}
>
{t("comm.unreadOnly")}
</button>
@@ -276,7 +276,7 @@ export function FilterBar({
<button
type="button"
onClick={() => onChangeFilters({ search: filters.search, channelType: "all", priority: "all", unreadOnly: false })}
className="text-[11px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
className="text-[11px] font-medium text-muted-foreground underline transition hover:text-foreground"
>
{t("comm.clearFilters")}
</button>
@@ -22,7 +22,7 @@ interface CommunicationHubProps {
function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channel"; onChangeScope: (scope: "global" | "channel") => void }) {
return (
<div
className="inline-flex items-center gap-0.5 rounded-full bg-[color:var(--panel)] p-0.5"
className="inline-flex items-center gap-0.5 rounded-full bg-card p-0.5"
role="radiogroup"
aria-label="Search scope"
>
@@ -34,8 +34,8 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("global") } }}
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "global"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}
>
All Channels
@@ -48,8 +48,8 @@ function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channe
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChangeScope("channel") } }}
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "channel"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
}`}
>
This Channel
@@ -9,7 +9,7 @@ import { useT } from "../../i18n"
function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) {
return (
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-[8px] bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" aria-label={label}>
<button type="button" onClick={onClick} disabled={disabled || sending} className="flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none" aria-label={label}>
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={18} weight="fill" />}
</button>
)
@@ -49,30 +49,30 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
const fromAgent = agents.find((a) => a.id === fromAgentId)
return (
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="border-t border-border bg-card px-4 py-3">
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
{channel === "direct" && !directRecipientId ? <div className="mb-2"><InlineNotice message="Select a direct conversation partner before sending a DM." /></div> : null}
<AnimatePresence>
{replyTo ? (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="mb-2 flex items-center gap-2 overflow-hidden rounded-[6px] bg-[color:var(--panel-soft)] px-3 py-2">
<ArrowBendUpLeft size={12} className="text-[color:var(--accent)]" />
<span className="min-w-0 flex-1 truncate text-[11px] text-[color:var(--muted-strong)]">Replying to <span className="font-semibold">{agents.find((a) => a.id === replyTo.fromAgentId)?.name ?? "agent"}</span>{replyTo.subject ? `: ${replyTo.subject}` : ""}</span>
<button type="button" onClick={onCancelReply} className="shrink-0 rounded-[4px] p-0.5 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={12} /></button>
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} className="mb-2 flex items-center gap-2 overflow-hidden rounded-lg bg-muted px-3 py-2">
<ArrowBendUpLeft size={12} className="text-primary" />
<span className="min-w-0 flex-1 truncate text-[11px] text-muted-foreground">Replying to <span className="font-semibold">{agents.find((a) => a.id === replyTo.fromAgentId)?.name ?? "agent"}</span>{replyTo.subject ? `: ${replyTo.subject}` : ""}</span>
<button type="button" onClick={onCancelReply} className="shrink-0 rounded-[4px] p-0.5 text-muted-foreground transition hover:text-foreground"><X size={12} /></button>
</motion.div>
) : null}
</AnimatePresence>
<div className="mb-2 flex items-center gap-2">
<AgentAvatar agent={fromAgent} size="sm" />
<select value={fromAgentId} onChange={(e) => setFromAgentId(e.target.value)} className="min-w-0 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[11px] font-medium text-[color:var(--text)]">
<select value={fromAgentId} onChange={(e) => setFromAgentId(e.target.value)} className="min-w-0 rounded-lg border border-border bg-card px-2 py-1 text-[11px] font-medium text-foreground">
{agents.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.role})</option>)}
</select>
{!replyTo ? <input type="text" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Subject (optional)" className="min-w-0 flex-1 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" /> : null}
{!replyTo ? <input type="text" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Subject (optional)" className="min-w-0 flex-1 rounded-lg border border-border bg-card px-2.5 py-1 text-[11px] text-foreground placeholder:text-muted-foreground" /> : null}
</div>
<div className="flex items-end gap-2">
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
<textarea ref={textareaRef} value={body} onChange={(e) => setBody(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-lg border border-border bg-muted px-3 py-2 text-[13px] text-foreground placeholder:text-muted-foreground focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--primary)" } as React.CSSProperties} />
<SendButton sending={sending} disabled={!body.trim() || !fromAgentId || (channel === "direct" && !directRecipientId)} onClick={() => void handleSend()} label="Send message" />
</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
<div className="mt-1 text-[11px] text-muted-foreground">Press {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+Enter to send</div>
</div>
)
}
@@ -84,10 +84,10 @@ export function TaskDiscussionComposer({ companyId, taskId, onSent }: { companyI
try { unwrap(await window.agentCompany.addComment({ companyId, taskId, authorName: "Board", body: body.trim() })); setBody(""); onSent() } catch (e) { setSendError(e instanceof Error ? e.message : "Failed to send.") } finally { setSending(false) }
}, [body, companyId, taskId, onSent])
return (
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="border-t border-border bg-card px-4 py-3">
{sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
<div className="flex items-end gap-2">
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--accent)" } as React.CSSProperties} />
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Reply in task discussion..." rows={2} className="min-h-[40px] min-w-0 flex-1 resize-none rounded-lg border border-border bg-muted px-3 py-2 text-[13px] text-foreground placeholder:text-muted-foreground focus:border-transparent focus:outline-none focus:ring-2" style={{ focusRingColor: "var(--primary)" } as React.CSSProperties} />
<SendButton sending={sending} disabled={!body.trim()} onClick={() => void handleSend()} label="Send task comment" />
</div>
</div>
@@ -96,7 +96,7 @@ export function TaskDiscussionComposer({ companyId, taskId, onSent }: { companyI
export function NoChannelComposer() {
const t = useT()
return <div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5"><div className="flex items-center gap-2 text-[11px] text-[color:var(--muted)]"><ChatCircle size={14} /><span>{t("comm.selectChannelSend")}</span></div></div>
return <div className="border-t border-border bg-card px-4 py-2.5"><div className="flex items-center gap-2 text-[11px] text-muted-foreground"><ChatCircle size={14} /><span>{t("comm.selectChannelSend")}</span></div></div>
}
export function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: { open: boolean; onClose: () => void; agents: AgentRecord[]; projects?: ProjectRecord[]; companyId: string; onSent: () => void }) {
@@ -130,8 +130,8 @@ export function ComposeModal({ open, onClose, agents, projects, companyId, onSen
<TextArea label={t("comm.message")} value={body} onChange={setBody} placeholder={t("comm.writeMessage")} rows={5} />
<Select label="Priority" value={priority} onChange={(v) => setPriority(v as MessagePriority)} options={[{ value: "low", label: "Low" }, { value: "normal", label: "Normal" }, { value: "urgent", label: "Urgent" }]} />
<div className="flex justify-end gap-3 pt-2">
<button type="button" onClick={onClose} className="rounded-[8px] px-3 py-2 text-[13px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]">{t("common.cancel")}</button>
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] font-semibold bg-[color:var(--accent)] text-[color:var(--text-on-accent)] transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none">
<button type="button" onClick={onClose} className="rounded-lg px-3 py-2 text-[13px] font-medium text-muted-foreground transition hover:bg-muted">{t("common.cancel")}</button>
<button type="button" onClick={() => void handleSend()} disabled={!body.trim() || !subject.trim() || !fromAgentId || (channel === "direct" && !toAgentId) || ((channel === "department" || channel === "project") && !channelTargetId) || sending} className="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-[13px] font-semibold bg-primary text-primary-foreground transition hover:brightness-90 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none">
{sending ? <svg className="h-4 w-4 animate-spin" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" opacity="0.3" /><path d="M14 8a6 6 0 0 0-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg> : <PaperPlaneTilt size={14} weight="fill" />}{t("comm.sendMessage")}
</button>
</div>
@@ -14,14 +14,14 @@ import { DEPARTMENT_COLORS } from "@shared/constants"
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
urgent: { label: "Urgent", color: "var(--danger)", bg: "var(--danger-soft)" },
normal: { label: "Normal", color: "var(--muted-strong)", bg: "var(--panel-soft)" },
low: { label: "Low", color: "var(--muted)", bg: "var(--panel-soft)" },
urgent: { label: "Urgent", color: "var(--destructive)", bg: "var(--destructive)" },
normal: { label: "Normal", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
low: { label: "Low", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
}
function getDepartmentColor(department: DepartmentId | null | undefined): string {
if (!department) return "var(--muted)"
return DEPARTMENT_COLORS[department] ?? "var(--muted)"
if (!department) return "var(--muted-foreground)"
return DEPARTMENT_COLORS[department] ?? "var(--muted-foreground)"
}
export const AgentAvatar = memo(function AgentAvatar({
@@ -32,7 +32,7 @@ export const AgentAvatar = memo(function AgentAvatar({
size?: "sm" | "md" | "lg"
}) {
const sizeClass = size === "sm" ? "h-7 w-7 text-[10px]" : size === "lg" ? "h-11 w-11 text-[14px]" : "h-9 w-9 text-[11px]"
const color = agent ? getDepartmentColor(agent.department) : "var(--muted)"
const color = agent ? getDepartmentColor(agent.department) : "var(--muted-foreground)"
const initials = agent ? getAgentInitials(agent.name) : "?"
return (
@@ -60,19 +60,19 @@ export const MessageBubble = memo(function MessageBubble({
}) {
const isUnread = !message.readAt
const isUrgent = message.priority === "urgent"
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted)"
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted-foreground)"
return (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={`group relative flex gap-3 rounded-[8px] px-4 py-3 transition ${
className={`group relative flex gap-3 rounded-lg px-4 py-3 transition ${
isThread ? "ml-11" : ""
} ${
isUnread
? "bg-[color:var(--panel)]"
: "hover:bg-[color:var(--panel-soft)]/40"
? "bg-card"
: "hover:bg-muted/40"
}`}
>
<AgentAvatar agent={agent} />
@@ -83,7 +83,7 @@ export const MessageBubble = memo(function MessageBubble({
{agent?.name ?? "Unknown Agent"}
</span>
{agent?.title ? (
<span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span>
<span className="truncate text-[11px] text-muted-foreground">{agent.title}</span>
) : null}
{agent?.department ? (
<span
@@ -93,7 +93,7 @@ export const MessageBubble = memo(function MessageBubble({
{agent.department.replace("_", " ")}
</span>
) : null}
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">
<span className="whitespace-nowrap shrink-0 text-[11px] text-muted-foreground">
{formatMessageTime(message.createdAt)}
</span>
{message.priority !== "normal" ? (
@@ -109,23 +109,23 @@ export const MessageBubble = memo(function MessageBubble({
</span>
) : null}
{isUnread ? (
<Circle size={8} weight="fill" className="shrink-0" style={{ color: "var(--accent)" }} aria-label="Unread" />
<Circle size={8} weight="fill" className="shrink-0" style={{ color: "var(--primary)" }} aria-label="Unread" />
) : (
<CheckCircle
size={10}
weight="fill"
className="shrink-0 text-[color:var(--muted)] opacity-0 group-hover:opacity-60 group-focus-within:opacity-60"
className="shrink-0 text-muted-foreground opacity-0 group-hover:opacity-60 group-focus-within:opacity-60"
/>
)}
</div>
{message.subject ? (
<div className="mt-0.5 break-words text-[13px] font-medium text-[color:var(--text)]">
<div className="mt-0.5 break-words text-[13px] font-medium text-foreground">
{message.subject}
</div>
) : null}
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-muted-foreground">
{message.body}
</div>
@@ -139,7 +139,7 @@ export const MessageBubble = memo(function MessageBubble({
type="button"
onClick={onReply}
className="flex items-center gap-1.5 text-[11px] font-medium transition hover:opacity-80"
style={{ color: "var(--accent)" }}
style={{ color: "var(--primary)" }}
>
<ChatCircle size={12} />
{replyCount} {replyCount === 1 ? "reply" : "replies"}
@@ -148,7 +148,7 @@ export const MessageBubble = memo(function MessageBubble({
<button
type="button"
onClick={onReply}
className="flex items-center gap-1 rounded-[6px] px-2 py-0.5 text-[11px] font-medium text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)] group-hover:opacity-100"
className="flex items-center gap-1 rounded-lg px-2 py-0.5 text-[11px] font-medium text-muted-foreground opacity-0 transition hover:bg-muted hover:text-foreground group-hover:opacity-100"
>
<ArrowBendUpLeft size={10} />
Reply
@@ -167,25 +167,25 @@ export function TaskCommentBubble({
agent: AgentRecord | undefined
}) {
const isBoard = !comment.authorAgentId
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted)"
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted-foreground)"
return (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.18 }}
className="group relative flex gap-3 rounded-[8px] bg-[color:var(--panel)] px-4 py-3"
className="group relative flex gap-3 rounded-lg bg-card px-4 py-3"
>
<AgentAvatar agent={agent} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-semibold" style={{ color: isBoard ? "var(--accent)" : deptColor }}>
<span className="truncate text-[13px] font-semibold" style={{ color: isBoard ? "var(--primary)" : deptColor }}>
{comment.authorName}
</span>
{agent?.title ? <span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span> : null}
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span>
{agent?.title ? <span className="truncate text-[11px] text-muted-foreground">{agent.title}</span> : null}
<span className="whitespace-nowrap shrink-0 text-[11px] text-muted-foreground">{formatMessageTime(comment.createdAt)}</span>
</div>
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="mt-1 break-words whitespace-pre-wrap text-[13px] leading-relaxed text-muted-foreground">
{comment.body}
</div>
</div>
@@ -207,9 +207,9 @@ function AttachmentList({ json }: { json: string }) {
{items.map((item, i) => (
<span
key={`${item.id}-${i}`}
className="inline-flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] px-2 py-1 text-[11px] font-medium text-[color:var(--muted-strong)]"
className="inline-flex items-center gap-1 rounded-lg bg-muted px-2 py-1 text-[11px] font-medium text-muted-foreground"
>
<At size={10} className="text-[color:var(--muted)]" />
<At size={10} className="text-muted-foreground" />
{item.title || `${item.type}:${item.id.slice(0, 8)}`}
</span>
))}
@@ -92,8 +92,8 @@ export function MessageList({
if (!activeChannel) {
return (
<div className="flex h-full flex-col items-center justify-center text-center">
<ChatCircle size={40} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
<ChatCircle size={40} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">
{t("comm.selectChannelView")}
</div>
</div>
@@ -103,8 +103,8 @@ export function MessageList({
if (activeChannel.kind === "task" && taskDiscussionComments.length === 0) {
return (
<div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
<Tray size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">
{t("comm.noDiscussionYet")}
</div>
</div>
@@ -114,14 +114,14 @@ export function MessageList({
if (activeChannel.kind !== "task" && filteredMessages.length === 0) {
return (
<div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
<Tray size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? t("comm.noMatchFilters")
: t("comm.noMessagesChannel")}
</div>
{(filters.search || filters.unreadOnly || filters.priority !== "all") ? (
<div className="mt-1 text-[13px] text-[color:var(--muted)]">{t("comm.adjustFilters")}</div>
<div className="mt-1 text-[13px] text-muted-foreground">{t("comm.adjustFilters")}</div>
) : null}
</div>
)
@@ -159,7 +159,7 @@ export function MessageList({
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
{item.type === "comment" ? (
<TaskCommentBubble
@@ -9,11 +9,11 @@ import { EmptyState, useReducedMotion } from "../ui"
import { useRovingTabIndex } from "../../lib/keyboard"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--accent)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" },
company: { label: "Company", icon: Megaphone, color: "var(--accent)" },
project: { label: "Project", icon: Hash, color: "var(--success)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" },
direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--primary)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-foreground)" },
company: { label: "Company", icon: Megaphone, color: "var(--primary)" },
project: { label: "Project", icon: Hash, color: "var(--primary)" },
incident: { label: "Incident", icon: Fire, color: "var(--destructive)" },
}
function highlightMatch(text: string, query: string): React.ReactNode {
@@ -23,7 +23,7 @@ function highlightMatch(text: string, query: string): React.ReactNode {
const parts = text.split(regex)
return parts.map((part, i) =>
regex.test(part)
? <mark key={i} className="rounded-[2px] bg-[color:var(--warn-soft)] px-0.5 text-[color:var(--text)]">{part}</mark>
? <mark key={i} className="rounded-[2px] bg-yellow-100 dark:bg-yellow-900/50 px-0.5 text-foreground">{part}</mark>
: part
)
}
@@ -31,11 +31,11 @@ function highlightMatch(text: string, query: string): React.ReactNode {
function SkeletonRow() {
return (
<div className="flex gap-3 px-3 py-3" style={{ height: 72 }}>
<div className="h-7 w-7 shrink-0 animate-pulse rounded-full bg-[color:var(--panel-soft)]" />
<div className="h-7 w-7 shrink-0 animate-pulse rounded-full bg-muted" />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="h-3 w-32 animate-pulse rounded bg-[color:var(--panel-soft)]" />
<div className="h-3 w-48 animate-pulse rounded bg-[color:var(--panel-soft)]" />
<div className="h-3 w-64 animate-pulse rounded bg-[color:var(--panel-soft)]" />
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
<div className="h-3 w-48 animate-pulse rounded bg-muted" />
<div className="h-3 w-64 animate-pulse rounded bg-muted" />
</div>
</div>
)
@@ -97,7 +97,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
return (
<motion.div className="flex h-full flex-col" {...animationProps}>
<div className="shrink-0 px-3 py-2">
<span className="text-[11px] font-semibold text-[color:var(--muted)]">
<span className="text-[11px] font-semibold text-muted-foreground">
{results.length} result{results.length !== 1 ? "s" : ""}
</span>
</div>
@@ -144,7 +144,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`cursor-pointer px-3 py-2 transition-colors duration-100 hover:bg-[color:var(--panel-soft)] focus:outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-inset ring-[color:var(--accent)]" : ""}`}
className={`cursor-pointer px-3 py-2 transition-colors duration-100 hover:bg-muted focus:outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-inset ring-[color:var(--primary)]" : ""}`}
>
<div className="flex items-start gap-2.5">
<AgentAvatar agent={agent} size="sm" />
@@ -152,7 +152,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
<div className="min-w-0 flex-1">
{/* Row 1: Agent name, channel badge, timestamp */}
<div className="flex items-center gap-2">
<span className="truncate text-[11px] font-semibold text-[color:var(--text)]">
<span className="truncate text-[11px] font-semibold text-foreground">
{agent?.name ?? "Unknown Agent"}
</span>
<span
@@ -162,18 +162,18 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
<ChannelIcon size={10} weight="bold" />
{config.label}
</span>
<span className="ml-auto whitespace-nowrap shrink-0 font-mono text-[11px] text-[color:var(--muted)]">
<span className="ml-auto whitespace-nowrap shrink-0 font-mono text-[11px] text-muted-foreground">
{formatMessageTime(message.createdAt)}
</span>
</div>
{/* Row 2: Subject */}
<div className="mt-0.5 truncate text-[13px] font-semibold text-[color:var(--text)]">
<div className="mt-0.5 truncate text-[13px] font-semibold text-foreground">
{highlightMatch(message.subject, query)}
</div>
{/* Row 3: Body snippet */}
<div className="mt-0.5 line-clamp-2 break-words text-[13px] leading-relaxed text-[color:var(--muted)]">
<div className="mt-0.5 line-clamp-2 break-words text-[13px] leading-relaxed text-muted-foreground">
{highlightMatch(message.body, query)}
</div>
</div>
@@ -81,16 +81,16 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
<ListRow key={document.id}>
<div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(document.id)}>
<div className="flex items-center gap-2 text-[13px] font-medium text-[color:var(--text)]">
<FileText size={15} className="text-[color:var(--accent)]" />
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<FileText size={15} className="text-primary" />
<span className="truncate">{document.title}</span>
</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{document.type.replaceAll("_", " ")} · v{document.version} · {document.updatedAt.slice(0, 10)}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{document.type.replaceAll("_", " ")} · v{document.version} · {document.updatedAt.slice(0, 10)}</div>
</button>
<div className="flex items-center gap-2">
<StatusPill status={document.status} />
<button type="button" aria-label="Edit document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete document" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
<button type="button" aria-label="Edit document" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(document.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete document" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(document.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
@@ -99,7 +99,7 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
<Modal open={editingId !== null} title={activeDocument ? activeDocument.title : "New document"} onClose={() => setEditingId(null)} width="760px">
<DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === "__new__" ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
</Modal>
<ConfirmDialog open={deleteId !== null} title="Delete document" message="This removes the document from the company knowledge surface." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
<ConfirmDialog open={deleteId !== null} title="Delete document" message="This removes the document from the company knowledge bg-card border border-border shadow-sm." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
</div>
);
}
@@ -69,16 +69,16 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
<ListRow key={entry.id}>
<div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(entry.id)}>
<div className="flex items-center gap-2 text-[13px] font-medium text-[color:var(--text)]">
<Lightbulb size={15} className="text-[color:var(--accent)]" />
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<Lightbulb size={15} className="text-primary" />
<span className="truncate">{entry.topic}</span>
</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{entry.category.replaceAll("_", " ")} · {entry.updatedAt.slice(0, 10)}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{entry.category.replaceAll("_", " ")} · {entry.updatedAt.slice(0, 10)}</div>
</button>
<div className="flex items-center gap-2">
<StatusPill status={entry.importance} />
<button type="button" aria-label="Edit entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete entry" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
<button type="button" aria-label="Edit entry" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(entry.id)}><NotePencil size={14} /></button>
<button type="button" aria-label="Delete entry" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(entry.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
@@ -83,16 +83,16 @@ export function MeetingsManager({ companyId, meetings, agents, onRefresh }: {
<ListRow key={meeting.id}>
<div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(meeting.id)}>
<div className="flex items-center gap-2 text-[13px] font-medium text-[color:var(--text)]">
<CalendarBlank size={15} className="text-[color:var(--accent)]" />
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<CalendarBlank size={15} className="text-primary" />
<span className="truncate">{meeting.title}</span>
</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{meeting.type.replaceAll("_", " ")} · {new Date(meeting.scheduledAt).toLocaleString()} · {meeting.durationMinutes} min</div>
<div className="mt-1 text-[11px] text-muted-foreground">{meeting.type.replaceAll("_", " ")} · {new Date(meeting.scheduledAt).toLocaleString()} · {meeting.durationMinutes} min</div>
</button>
<div className="flex items-center gap-2">
<StatusPill status={meeting.status} />
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(meeting.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(meeting.id)}><Trash size={14} /></button>
<button type="button" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(meeting.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(meeting.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
@@ -13,8 +13,8 @@ export function SectionHeader({
return (
<div className="mb-4 flex items-center justify-between">
<div>
<div className="text-[16px] font-semibold text-[color:var(--text)]">{title}</div>
<div className="text-[12px] text-[color:var(--muted)]">{count} item{count === 1 ? "" : "s"}</div>
<div className="text-[16px] font-semibold text-foreground">{title}</div>
<div className="text-[12px] text-muted-foreground">{count} item{count === 1 ? "" : "s"}</div>
</div>
<ActionButton label="New" icon={Plus} tone="accent" onClick={action} />
</div>
@@ -75,16 +75,16 @@ export function SprintManager({ companyId, sprints, onRefresh }: {
<ListRow key={sprint.id}>
<div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(sprint.id)}>
<div className="flex items-center gap-2 text-[13px] font-medium text-[color:var(--text)]">
<Rows size={15} className="text-[color:var(--accent)]" />
<div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<Rows size={15} className="text-primary" />
<span className="truncate">{sprint.name}</span>
</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">{sprint.startDate} → {sprint.endDate} · {sprint.completedPoints}/{sprint.velocityPoints} pts</div>
<div className="mt-1 text-[11px] text-muted-foreground">{sprint.startDate} → {sprint.endDate} · {sprint.completedPoints}/{sprint.velocityPoints} pts</div>
</button>
<div className="flex items-center gap-2">
<StatusPill status={sprint.status} />
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--text)]" onClick={() => setEditingId(sprint.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(sprint.id)}><Trash size={14} /></button>
<button type="button" className="rounded p-1 text-muted-foreground hover:text-foreground" onClick={() => setEditingId(sprint.id)}><NotePencil size={14} /></button>
<button type="button" className="rounded p-1 text-muted-foreground hover:text-destructive" onClick={() => setDeleteId(sprint.id)}><Trash size={14} /></button>
</div>
</div>
</ListRow>
+14 -14
View File
@@ -137,7 +137,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
<div className="grid gap-4">
{submitError ? <InlineNotice message={submitError} /> : null}
{needsLogin ? (
<div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-4 text-[12px] text-[color:var(--warn)]">
<div className="rounded-lg border border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 px-4 py-4 text-[12px] text-yellow-600 dark:text-yellow-400">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em]">{authActionCopy.title}</div>
<div className="mt-1.5 leading-relaxed">{authActionCopy.message}</div>
<div className="mt-3">
@@ -163,17 +163,17 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.status")}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{t("conn.status")}</div>
<div className="flex items-center gap-3">
<StatusPill status={connector.status} />
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
<span className="text-[12px] text-muted-foreground">{connector.version ?? "version unknown"}</span>
</div>
</div>
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.capabilityMatrix")}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{t("conn.capabilityMatrix")}</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
<div key={key} className={`rounded-[6px] border px-3 py-2 text-[12px] ${value ? "border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] text-[color:var(--accent)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] text-[color:var(--muted)]"}`}>
<div key={key} className={`rounded-lg border px-3 py-2 text-[12px] ${value ? "border-[color:var(--primary)] border-opacity-20 bg-accent text-primary" : "border-border bg-muted text-muted-foreground"}`}>
{key}
</div>
))}
@@ -182,7 +182,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
</div>
<div>
<Input label="Command" value={connector.command} onChange={() => undefined} readOnly />
<div className="mt-1.5 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="mt-1.5 text-[12px] leading-relaxed text-muted-foreground">
AgentCompany pins connector execution to the audited binary for this runtime. Command overrides are not accepted from the renderer.
</div>
</div>
@@ -195,13 +195,13 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
options={modelSelectOptions}
/>
{loadingModelOptions ? (
<div className="text-[12px] text-[color:var(--muted)]">Loading available models...</div>
<div className="text-[12px] text-muted-foreground">Loading available models...</div>
) : null}
{modelOptionsError ? (
<InlineNotice message={modelOptionsError} />
) : null}
{requiresExplicitModel && !model.trim() ? (
<div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
<div className="rounded-lg border border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3 text-[12px] text-yellow-600 dark:text-yellow-400">
OpenCode is detected, but it still needs an explicit provider/model selection before AgentCompany can mark it ready.
</div>
) : null}
@@ -211,13 +211,13 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
)}
<TextArea label="Secret bindings" value={envBindingText} onChange={setEnvBindingText} rows={4} placeholder={"OPENAI_API_KEY=my-vault-key\nANTHROPIC_API_KEY=my-anthropic-key"} />
<TextArea label="Operational notes" value={notes} onChange={setNotes} rows={4} />
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] text-[color:var(--muted-strong)]">
<div className="rounded-lg border border-border bg-muted px-4 py-3 text-[12px] text-muted-foreground">
Connector health checks run automatically on app launch, after save, and after terminal login is opened.
{autoChecking ? " Refreshing connector state..." : ""}
</div>
{connector.lastError ? (
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Latest health-check detail</div>
<div className="rounded-lg border border-border bg-muted px-4 py-3 text-[12px] leading-relaxed text-muted-foreground">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Latest health-check detail</div>
<div>{connector.lastError}</div>
</div>
) : null}
@@ -249,9 +249,9 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
disabled={requiresExplicitModel && !model.trim()}
/>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-4 text-[12px] leading-relaxed text-[color:var(--muted)]">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Configuration doc</div>
<pre className="mono whitespace-pre-wrap">{connector.configurationDoc}</pre>
<div className="rounded-lg border border-border bg-muted px-4 py-4 text-[12px] leading-relaxed text-muted-foreground">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Configuration doc</div>
<pre className="font-mono whitespace-pre-wrap">{connector.configurationDoc}</pre>
</div>
</div>
);
@@ -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} />
+37 -37
View File
@@ -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>
@@ -43,15 +43,15 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="relative flex-1">
<MagnifyingGlass size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[color:var(--muted)]" />
<input type="text" className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2.5 pl-8 pr-3.5 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]" value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t("exec.searchActivity")} />
<MagnifyingGlass size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input type="text" 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 py-2.5 pl-8 pr-3.5 text-[13px] text-foreground placeholder:text-muted-foreground" value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t("exec.searchActivity")} />
</div>
<Select value={entityFilter} onChange={setEntityFilter} options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? t("exec.allTypes") : v }))} />
</div>
{filtered.length === 0 ? <EmptyState title={t("exec.noMatchingActivity")} /> : (
<div
ref={parentRef}
className="h-full max-h-[600px] overflow-auto border-y border-[color:var(--line)]"
className="h-full max-h-[600px] overflow-auto border-y border-border"
role="listbox"
aria-label="Activity log"
onKeyDown={handleKeyDown}
@@ -81,17 +81,17 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none border-b border-border px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<EntityIcon size={14} weight="duotone" className="shrink-0 text-[color:var(--muted)]" />
<div className="truncate text-[13px] text-[color:var(--text)]">{entry.action}</div>
<EntityIcon size={14} weight="duotone" className="shrink-0 text-muted-foreground" />
<div className="truncate text-[13px] text-foreground">{entry.action}</div>
</div>
<div className="mono text-[11px] text-[color:var(--muted)]">{formatTime(entry.createdAt)}</div>
<div className="font-mono text-[11px] text-muted-foreground">{formatTime(entry.createdAt)}</div>
</div>
<div className="flex items-center gap-2 text-[13px] text-[color:var(--muted)]">
<span className="font-medium text-[color:var(--muted-strong)]">{entry.actor}</span>
<div className="flex items-center gap-2 text-[13px] text-muted-foreground">
<span className="font-medium text-muted-foreground">{entry.actor}</span>
<span>·</span>
<span className="truncate">{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
</div>
@@ -11,12 +11,12 @@ import { SocialDraftPreview } from "./SocialDraftPreview"
import { useAppStore } from "../../state/store"
function getTypeBadgeClass(type: string): string {
if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
if (type === "approve_ceo_strategy") return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
if (type === "secret_access") return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
if (type === "social_post") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
if (type === "deliverable_review") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
return "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
if (type === "hire_agent") return "bg-accent text-primary"
if (type === "approve_ceo_strategy") return "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
if (type === "secret_access") return "bg-red-100 dark:bg-red-900/50 text-destructive"
if (type === "social_post") return "bg-accent text-primary"
if (type === "deliverable_review") return "bg-accent text-primary"
return "bg-muted text-muted-foreground"
}
export function ApprovalInspector({ approval, agents, tasks, documents, companyId, onDecide, onDeleted }: {
@@ -90,30 +90,30 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
<div className="space-y-6">
<ConfirmDialog open={confirmDelete} title={t("exec.deleteApproval")} message={t("exec.deleteApprovalConfirm")} errorMessage={actionError} onConfirm={() => void handleDelete()} onCancel={() => { setActionError(null); setConfirmDelete(false) }} />
{actionError && !confirmDelete ? <InlineNotice message={actionError} /> : null}
<div className="border-y border-[color:var(--line)] py-4 space-y-4">
<div className="border-y border-border py-4 space-y-4">
<div className="flex items-center gap-3">
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] ${getTypeBadgeClass(approval.type)}`}>{approval.type.replaceAll("_", " ")}</span>
<StatusPill status={approval.state} />
</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
{approval.impactSummary ? <div className="text-[13px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null}
<div className="text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
{approval.impactSummary ? <div className="text-[13px] text-muted-foreground">{approval.impactSummary}</div> : null}
<div className="grid gap-2 text-[13px] md:grid-cols-2">
<div className="flex items-center gap-2 text-[color:var(--muted)]"><Robot size={14} />Requested by: <span className="font-medium text-[color:var(--text)]">{requestingAgent?.name ?? "Unknown"}</span>{requestingAgent?.role ? <span className="text-[10px] text-[color:var(--muted)]">({requestingAgent.role})</span> : null}</div>
{relatedTask ? <div className="flex items-center gap-2 text-[color:var(--muted)]"><Briefcase size={14} />Task: <span className="font-medium text-[color:var(--text)]">{relatedTask.title}</span></div> : null}
<div className="text-[color:var(--muted)]">Created: <span className="mono text-[color:var(--text)]">{formatTime(approval.createdAt)}</span></div>
{approval.updatedAt !== approval.createdAt ? <div className="text-[color:var(--muted)]">Updated: <span className="mono text-[color:var(--text)]">{formatTime(approval.updatedAt)}</span></div> : null}
<div className="flex items-center gap-2 text-muted-foreground"><Robot size={14} />Requested by: <span className="font-medium text-foreground">{requestingAgent?.name ?? "Unknown"}</span>{requestingAgent?.role ? <span className="text-[10px] text-muted-foreground">({requestingAgent.role})</span> : null}</div>
{relatedTask ? <div className="flex items-center gap-2 text-muted-foreground"><Briefcase size={14} />Task: <span className="font-medium text-foreground">{relatedTask.title}</span></div> : null}
<div className="text-muted-foreground">Created: <span className="font-mono text-foreground">{formatTime(approval.createdAt)}</span></div>
{approval.updatedAt !== approval.createdAt ? <div className="text-muted-foreground">Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span></div> : null}
</div>
{approval.type === "hire_agent" ? (() => { const payload = parseApprovalPayloadJson(approval); const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null; const reqSnap = payload?.requestedConfigurationSnapshot && typeof payload.requestedConfigurationSnapshot === "object" && !Array.isArray(payload.requestedConfigurationSnapshot) ? payload.requestedConfigurationSnapshot as Record<string, unknown> : null; const proposedAgent = (approval.relatedAgentId ? (agents.find((a) => a.id === approval.relatedAgentId) ?? null) : null) ?? (payloadAgentId ? (agents.find((a) => a.id === payloadAgentId) ?? null) : null); const mgrId = proposedAgent?.reportsTo ?? (typeof reqSnap?.reportsTo === "string" ? reqSnap.reportsTo : null); const mgr = mgrId ? (agents.find((a) => a.id === mgrId) ?? null) : null; if (!proposedAgent && !reqSnap) return null; return (
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-30 bg-[color:var(--accent-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent)]">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
<div><span className="text-[color:var(--muted)]">Name:</span> <span className="font-medium text-[color:var(--text)]">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
<div><span className="text-[color:var(--muted)]">Role:</span> <span className="text-[color:var(--text)]">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-[color:var(--muted)]">Title:</span> <span className="text-[color:var(--text)]">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
{(proposedAgent?.capabilities || typeof reqSnap?.capabilities === "string") ? <div><span className="text-[color:var(--muted)]">Capabilities:</span> <span className="text-[color:var(--text)]">{proposedAgent?.capabilities || reqSnap?.capabilities as string}</span></div> : null}
<div><span className="text-[color:var(--muted)]">Connector:</span> <span className="mono text-[color:var(--text)]">{proposedAgent?.connectorId ?? (typeof reqSnap?.connectorId === "string" ? reqSnap.connectorId : "Unknown")}</span></div>
<div><span className="text-[color:var(--muted)]">Budget:</span> <span className="mono text-[color:var(--text)]">{formatMoney(proposedAgent?.budgetMonthlyUsd ?? (typeof reqSnap?.budgetMonthlyUsd === "number" ? reqSnap.budgetMonthlyUsd : 0))}/mo</span></div>
{mgr ? <div><span className="text-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{mgr.name}</span></div> : null}
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-30 bg-accent px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
<div><span className="text-muted-foreground">Name:</span> <span className="font-medium text-foreground">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
<div><span className="text-muted-foreground">Role:</span> <span className="text-foreground">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-muted-foreground">Title:</span> <span className="text-foreground">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
{(proposedAgent?.capabilities || typeof reqSnap?.capabilities === "string") ? <div><span className="text-muted-foreground">Capabilities:</span> <span className="text-foreground">{proposedAgent?.capabilities || reqSnap?.capabilities as string}</span></div> : null}
<div><span className="text-muted-foreground">Connector:</span> <span className="font-mono text-foreground">{proposedAgent?.connectorId ?? (typeof reqSnap?.connectorId === "string" ? reqSnap.connectorId : "Unknown")}</span></div>
<div><span className="text-muted-foreground">Budget:</span> <span className="font-mono text-foreground">{formatMoney(proposedAgent?.budgetMonthlyUsd ?? (typeof reqSnap?.budgetMonthlyUsd === "number" ? reqSnap.budgetMonthlyUsd : 0))}/mo</span></div>
{mgr ? <div><span className="text-muted-foreground">Reports to:</span> <span className="text-foreground">{mgr.name}</span></div> : null}
</div></div>) })() : null}
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
{approval.type === "approve_ceo_strategy" ? <div 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="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-yellow-600 dark:text-yellow-400">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">{approval.payloadSummary}</div></div> : null}
{approval.type === "deliverable_review" ? <DeliverableReviewPanel approval={approval} documents={documents} agents={agents} /> : null}
{isSocialPost && approval.payloadJson ? (
<SocialDraftPreview
@@ -133,37 +133,37 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
role="radio"
aria-checked={scheduleMode === "immediate"}
tabIndex={0}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "immediate" ? "border-[color:var(--accent)]" : "border-[color:var(--line)]"}`}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "immediate" ? "border-[color:var(--primary)]" : "border-border"}`}
onClick={() => { setScheduleMode("immediate"); setScheduleError(null) }}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { setScheduleMode("immediate"); setScheduleError(null) } }}
>
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
{scheduleMode === "immediate" ? <span className="h-2 w-2 rounded-full bg-primary" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-[color:var(--text)]">Post immediately</span>
<span className="text-[13px] font-medium text-foreground">Post immediately</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<span
role="radio"
aria-checked={scheduleMode === "scheduled"}
tabIndex={0}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "scheduled" ? "border-[color:var(--accent)]" : "border-[color:var(--line)]"}`}
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${scheduleMode === "scheduled" ? "border-[color:var(--primary)]" : "border-border"}`}
onClick={() => setScheduleMode("scheduled")}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setScheduleMode("scheduled") }}
>
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
{scheduleMode === "scheduled" ? <span className="h-2 w-2 rounded-full bg-primary" aria-hidden="true" /> : null}
</span>
<span className="text-[13px] font-medium text-[color:var(--text)]">Schedule for later</span>
<span className="text-[13px] font-medium text-foreground">Schedule for later</span>
</label>
</div>
{scheduleMode === "scheduled" ? (
<div className="flex items-center gap-2">
<Clock size={16} className="text-[color:var(--muted)]" />
<Clock size={16} className="text-muted-foreground" />
<input
type="datetime-local"
value={scheduledAt}
min={minDatetime}
onChange={(e) => { setScheduledAt(e.target.value); setScheduleError(null) }}
className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[13px] text-[color:var(--text)] focus:border-[color:var(--accent)] focus:outline-none"
className="rounded-lg border border-border bg-card px-3.5 py-2.5 text-[13px] text-foreground focus:border-[color:var(--primary)] focus:outline-none"
/>
</div>
) : null}
@@ -171,7 +171,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
</div>
) : null}
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
{!isDecided ? <div><label className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Decision note</label><TextArea value={decisionNote} onChange={setDecisionNote} placeholder="Add a note (optional)" rows={3} /></div> : null}
<div className="flex flex-wrap gap-3">
{!isDecided ? <>
<ActionButton
@@ -186,7 +186,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
</> : null}
{isDecided ? <ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" loading={acting === "delete"} /> : null}
</div>
{approval.decisionNote ? <div className="border-t border-[color:var(--line)] pt-4 text-[13px] text-[color:var(--muted-strong)]">Last note: {approval.decisionNote}</div> : null}
{approval.decisionNote ? <div className="border-t border-border pt-4 text-[13px] text-muted-foreground">Last note: {approval.decisionNote}</div> : null}
</div>
)
}
@@ -200,36 +200,36 @@ function DeliverableReviewPanel({ approval, documents, agents }: { approval: App
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent)]">Deliverable Review</div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Deliverable Review</div>
{fullDocs.length > 0 ? fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return (
<div key={doc.id} className="rounded-[8px] border border-[color:var(--line)] overflow-hidden">
<div key={doc.id} className="rounded-lg border border-border overflow-hidden">
<button
type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
className="flex w-full cursor-pointer items-center gap-3 bg-[color:var(--panel-soft)] px-4 py-3 text-left transition hover:bg-[color:var(--panel)]"
className="flex w-full cursor-pointer items-center gap-3 bg-muted px-4 py-3 text-left transition hover:bg-card"
>
<span className="shrink-0 rounded-full bg-[color:var(--accent-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[color:var(--accent)]">{doc.type.replaceAll("_", " ")}</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-[color:var(--text)]">{doc.title}</span>
{isExpanded ? <CaretUp size={14} className="shrink-0 text-[color:var(--muted)]" /> : <CaretDown size={14} className="shrink-0 text-[color:var(--muted)]" />}
<span className="shrink-0 rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">{doc.type.replaceAll("_", " ")}</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">{doc.title}</span>
{isExpanded ? <CaretUp size={14} className="shrink-0 text-muted-foreground" /> : <CaretDown size={14} className="shrink-0 text-muted-foreground" />}
</button>
{isExpanded ? (
<div className="border-t border-[color:var(--line)] px-4 py-3">
{author ? <div className="mb-2 text-[11px] text-[color:var(--muted)]">Author: <span className="font-medium text-[color:var(--text)]">{author.name}</span> ({author.role})</div> : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{doc.content || "No content."}</div>
<div className="border-t border-border px-4 py-3">
{author ? <div className="mb-2 text-[11px] text-muted-foreground">Author: <span className="font-medium text-foreground">{author.name}</span> ({author.role})</div> : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">{doc.content || "No content."}</div>
</div>
) : null}
</div>
)
}) : docRefs.length > 0 ? docRefs.map((doc) => (
<div key={doc.id} className="flex items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px]">
<span className="rounded-full bg-[color:var(--accent-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[color:var(--accent)]">{doc.type.replaceAll("_", " ")}</span>
<span className="font-medium text-[color:var(--text)]">{doc.title}</span>
<div key={doc.id} className="flex items-center gap-2 rounded-lg border border-border bg-muted px-4 py-3 text-[12px]">
<span className="rounded-full bg-accent px-2 py-0.5 text-[10px] font-semibold uppercase text-primary">{doc.type.replaceAll("_", " ")}</span>
<span className="font-medium text-foreground">{doc.title}</span>
</div>
)) : <div className="text-[12px] text-[color:var(--muted)]">{approval.payloadSummary}</div>}
{approval.impactSummary ? <div className="text-[11px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null}
)) : <div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>}
{approval.impactSummary ? <div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div> : null}
</div>
)
}
@@ -21,7 +21,7 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
return (
<div
ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]"
className="h-full overflow-auto border-y border-border"
role="listbox"
aria-label="Pending approvals"
onKeyDown={handleKeyDown}
@@ -50,16 +50,16 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<button
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedApprovalId === approval.id ? "bg-[color:var(--warn-soft)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
className={`relative w-full border-b border-border px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedApprovalId === approval.id ? "bg-yellow-100 dark:bg-yellow-900/50" : "bg-transparent hover:bg-muted"}`}
onClick={() => onSelect(approval.id)}
tabIndex={-1}
>
{selectedApprovalId === approval.id ? <span className="absolute inset-y-0 left-0 w-[3px] bg-[color:var(--warn)]" /> : null}
{selectedApprovalId === approval.id ? <span className="absolute inset-y-0 left-0 w-[3px] bg-yellow-600 dark:bg-yellow-400" /> : null}
<div className="flex items-center justify-between gap-4">
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
<div className={`truncate text-[13px] text-foreground ${selectedApprovalId === approval.id ? "font-semibold" : "font-medium"}`}>{approval.payloadSummary}</div>
<StatusPill status={approval.state} />
</div>
</button>
@@ -28,14 +28,14 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
return (
<div className="space-y-4">
<div className="border-y border-[color:var(--line)]">
<div className="border-y border-border">
<div className="flex items-end justify-between gap-4 py-3">
<div className="text-[12px] text-[color:var(--muted)]">Known spend</div>
<div className="mono text-[22px] tracking-[-0.03em] text-[color:var(--text)]">{formatMoney(knownSpend)}</div>
<div className="text-[12px] text-muted-foreground">Known spend</div>
<div className="font-mono text-[22px] tracking-[-0.03em] text-foreground">{formatMoney(knownSpend)}</div>
</div>
<div className="flex items-end justify-between gap-4 border-t border-[color:var(--line)] py-3">
<div className="text-[12px] text-[color:var(--muted)]">Unattributed entries</div>
<div className="mono text-[18px] text-[color:var(--text)]">{String(unattributedCount)}</div>
<div className="flex items-end justify-between gap-4 border-t border-border py-3">
<div className="text-[12px] text-muted-foreground">Unattributed entries</div>
<div className="font-mono text-[18px] text-foreground">{String(unattributedCount)}</div>
</div>
</div>
@@ -45,8 +45,8 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
const pct = status.amount > 0 ? (status.spent / status.amount) * 100 : 0
const isWarning = pct > 80 && pct <= 100
const isDanger = pct > 100
const barColor = isDanger ? "bg-[color:var(--danger)]" : isWarning ? "bg-[color:var(--warn)]" : "bg-[color:var(--success)]"
const nameColor = isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--text)]"
const barColor = isDanger ? "bg-destructive" : isWarning ? "bg-yellow-600 dark:bg-yellow-400" : "bg-green-600 dark:bg-green-400"
const nameColor = isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-foreground"
const scopeLabel = status.scopeType === "company"
? "Company"
@@ -58,18 +58,18 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
<ListRow key={status.policyId}>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[color:var(--muted-strong)]">
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-muted-foreground">
{status.scopeType}
</span>
<div className={`text-[14px] font-medium ${nameColor}`}>{scopeLabel}</div>
{status.hasOpenIncident ? <Warning size={14} weight="fill" className="text-[color:var(--warn)]" /> : null}
{status.hasOpenIncident ? <Warning size={14} weight="fill" className="text-yellow-600 dark:text-yellow-400" /> : null}
</div>
<div className="flex items-center gap-2">
<div className="mono text-[12px] text-[color:var(--text)]">{formatMoney(status.spent / 100)} / {formatMoney(status.amount / 100)}</div>
<span className={`mono text-[11px] font-semibold ${isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--muted)]"}`}>{Math.round(pct)}%</span>
<div className="font-mono text-[12px] text-foreground">{formatMoney(status.spent / 100)} / {formatMoney(status.amount / 100)}</div>
<span className={`font-mono text-[11px] font-semibold ${isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-muted-foreground"}`}>{Math.round(pct)}%</span>
</div>
</div>
<div className="h-2 rounded-full bg-[color:var(--line)]"><div className={`h-2 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} /></div>
<div className="h-2 rounded-full bg-[color:var(--border)]"><div className={`h-2 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} /></div>
</ListRow>
)
})}
@@ -79,22 +79,22 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
{agents.map((agent) => {
const pct = agent.budgetMonthlyUsd > 0 ? (agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100 : 0
const isWarning = pct > 80 && pct <= 100; const isDanger = pct > 100
const barColor = isDanger ? "bg-[color:var(--danger)]" : isWarning ? "bg-[color:var(--warn)]" : "bg-[color:var(--success)]"
const nameColor = isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--text)]"
const barColor = isDanger ? "bg-destructive" : isWarning ? "bg-yellow-600 dark:bg-yellow-400" : "bg-green-600 dark:bg-green-400"
const nameColor = isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-foreground"
return (
<ListRow key={agent.id}>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`text-[14px] font-medium ${nameColor}`}>{agent.name}</div>
{isDanger ? <Warning size={14} weight="fill" className="text-[color:var(--danger)]" /> : null}
{isWarning ? <Warning size={14} weight="fill" className="text-[color:var(--warn)]" /> : null}
{isDanger ? <Warning size={14} weight="fill" className="text-destructive" /> : null}
{isWarning ? <Warning size={14} weight="fill" className="text-yellow-600 dark:text-yellow-400" /> : null}
</div>
<div className="flex items-center gap-2">
<div className="mono text-[12px] text-[color:var(--text)]">{formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)}</div>
<span className={`mono text-[11px] font-semibold ${isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--muted)]"}`}>{Math.round(pct)}%</span>
<div className="font-mono text-[12px] text-foreground">{formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)}</div>
<span className={`font-mono text-[11px] font-semibold ${isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-muted-foreground"}`}>{Math.round(pct)}%</span>
</div>
</div>
<div className="h-2 rounded-full bg-[color:var(--line)]"><div className={`h-2 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} /></div>
<div className="h-2 rounded-full bg-[color:var(--border)]"><div className={`h-2 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} /></div>
</ListRow>
)
})}
@@ -36,7 +36,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
</div>
<div
ref={parentRef}
className="h-full max-h-[600px] overflow-auto border-y border-[color:var(--line)]"
className="h-full max-h-[600px] overflow-auto border-y border-border"
role="listbox"
aria-label="Cost entries"
onKeyDown={handleKeyDown}
@@ -66,11 +66,11 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none border-b border-border px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<div className="flex items-center justify-between">
<div className="text-[13px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div>
<div className="mono tabular-nums text-[13px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{formatMoney(total)}</div>
<div className="text-[13px] font-semibold text-foreground">Total ({filtered.length} entries)</div>
<div className="font-mono tabular-nums text-[13px] font-bold tracking-[-0.02em] text-foreground">{formatMoney(total)}</div>
</div>
</div>
)
@@ -91,16 +91,16 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none border-b border-[color:var(--line)] px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none border-b border-border px-4 py-3 last:border-b-0 ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] text-[color:var(--text)]">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
<div className="text-[11px] text-[color:var(--muted)]">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
<div className="truncate text-[13px] text-foreground">{runs.find((run) => run.id === entry.runId)?.summary || "Run cost"}</div>
<div className="text-[11px] text-muted-foreground">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
</div>
<div className="shrink-0 text-right">
<div className="mono tabular-nums text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div>
{entry.unattributed ? <div className="text-[11px] text-[color:var(--warn)]">unattributed</div> : null}
<div className="font-mono tabular-nums text-[13px] text-foreground">{formatMoney(entry.amountUsd)}</div>
{entry.unattributed ? <div className="text-[11px] text-yellow-600 dark:text-yellow-400">unattributed</div> : null}
</div>
</div>
</div>
@@ -11,10 +11,10 @@ interface IncidentListProps {
}
const STATUS_STYLES: Record<string, string> = {
open: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]",
resolved: "text-[color:var(--success)] bg-[color:var(--success-soft)]",
auto_resolved_limit_increase: "text-[color:var(--accent)] bg-[color:var(--accent-soft)]",
auto_closed: "text-[color:var(--muted-strong)] bg-[color:var(--panel-soft)]",
open: "text-destructive bg-red-100 dark:bg-red-900/50",
resolved: "text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/50",
auto_resolved_limit_increase: "text-primary bg-accent",
auto_closed: "text-muted-foreground bg-muted",
};
const STATUS_LABELS: Record<string, string> = {
@@ -56,8 +56,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between px-4 py-3">
<h3 className="text-[15px] font-semibold text-[color:var(--text)]">Budget Incidents</h3>
<div className="flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] p-0.5">
<h3 className="text-[15px] font-semibold text-foreground">Budget Incidents</h3>
<div className="flex items-center gap-1 rounded-lg bg-muted p-0.5">
{(["open", "all"] as const).map((f) => (
<button
key={f}
@@ -65,8 +65,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
onClick={() => setFilter(f)}
className={`rounded-[5px] px-3 py-1 text-[12px] font-medium transition ${
filter === f
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{f === "open" ? "Open" : "All"}
@@ -93,8 +93,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${
isWarn
? "text-[color:var(--warn)] bg-[color:var(--warn-soft)]"
: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]"
? "text-yellow-600 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/50"
: "text-destructive bg-red-100 dark:bg-red-900/50"
}`}
>
{isWarn ? <Warning size={10} weight="bold" /> : null}
@@ -105,13 +105,13 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
</span>
</div>
<div className="mt-1.5 flex items-center gap-2 text-[12px] text-[color:var(--muted-strong)]">
<div className="mt-1.5 flex items-center gap-2 text-[12px] text-muted-foreground">
<span className="capitalize">{incident.scopeType}</span>
<span className="text-[color:var(--muted)]">&middot;</span>
<span className="font-medium text-[color:var(--text)]">{getScopeName(incident.scopeType, incident.scopeId)}</span>
<span className="text-muted-foreground">&middot;</span>
<span className="font-medium text-foreground">{getScopeName(incident.scopeType, incident.scopeId)}</span>
</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
<div className="mt-1 flex items-center gap-3 text-[11px] text-muted-foreground">
<span className="font-[family-name:var(--font-mono)]" style={{ fontVariantNumeric: "tabular-nums" }}>
${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)}
</span>
@@ -13,11 +13,11 @@ export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agen
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null
const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
return (
<div key={task.id} className="flex items-start gap-3 border-b border-[color:var(--line)] px-4 py-3 last:border-b-0">
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-[color:var(--accent)]" />
<div key={task.id} className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-b-0">
<Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-primary" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{task.title}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> &rarr; reviewed by <span className="text-[color:var(--muted-strong)]">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
<div className="truncate text-[13px] font-medium text-foreground">{task.title}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">{assignee ? assignee.name : "Unassigned"}{reviewer ? <> &rarr; reviewed by <span className="text-muted-foreground">{reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</span></> : " · awaiting review"}</div>
</div>
</div>
)
@@ -136,9 +136,9 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
type="checkbox"
checked={hardStopEnabled}
onChange={(e) => setHardStopEnabled(e.target.checked)}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<span className="text-[13px] text-[color:var(--text)]">Enable hard stop (block new runs at 100%)</span>
<span className="text-[13px] text-foreground">Enable hard stop (block new runs at 100%)</span>
</label>
<label className="flex items-center gap-2.5">
@@ -146,15 +146,15 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
type="checkbox"
checked={notifyEnabled}
onChange={(e) => setNotifyEnabled(e.target.checked)}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<span className="text-[13px] text-[color:var(--text)]">Send notifications</span>
<span className="text-[13px] text-foreground">Send notifications</span>
</label>
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
className="rounded-[8px] px-4 py-2 text-[13px] font-medium text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
className="rounded-lg px-4 py-2 text-[13px] font-medium text-muted-foreground transition hover:text-foreground"
onClick={onClose}
>
Cancel
@@ -91,7 +91,7 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between px-4 py-3">
<h3 className="text-[15px] font-semibold text-[color:var(--text)]">Budget Policies</h3>
<h3 className="text-[15px] font-semibold text-foreground">Budget Policies</h3>
<ActionButton label="Add Policy" icon={Plus} onClick={handleAdd} />
</div>
<div className="flex-1 overflow-y-auto px-4 pb-4">
@@ -99,20 +99,20 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
{policies.map((policy) => {
const util = getUtilization(policy.id);
const pct = util?.pct ?? 0;
const barColor = pct > 100 ? "bg-[color:var(--danger)]" : pct > 80 ? "bg-[color:var(--warn)]" : "bg-[color:var(--success)]";
const barColor = pct > 100 ? "bg-destructive" : pct > 80 ? "bg-yellow-600 dark:bg-yellow-400" : "bg-green-600 dark:bg-green-400";
return (
<ListRow key={policy.id} onClick={() => handleEdit(policy)}>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-[color:var(--panel-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[color:var(--muted-strong)]">
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-muted-foreground">
{policy.scopeType}
</span>
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{getScopeName(policy)}</span>
{policy.hardStopEnabled ? <ShieldCheck size={14} className="shrink-0 text-[color:var(--accent)]" /> : null}
{util?.hasIncident ? <Warning size={14} weight="fill" className="shrink-0 text-[color:var(--warn)]" /> : null}
<span className="truncate text-[13px] font-medium text-foreground">{getScopeName(policy)}</span>
{policy.hardStopEnabled ? <ShieldCheck size={14} className="shrink-0 text-primary" /> : null}
{util?.hasIncident ? <Warning size={14} weight="fill" className="shrink-0 text-yellow-600 dark:text-yellow-400" /> : null}
</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
<div className="mt-1 flex items-center gap-3 text-[11px] text-muted-foreground">
<span className="font-[family-name:var(--font-mono)]" style={{ fontVariantNumeric: "tabular-nums" }}>
${(policy.amount / 100).toFixed(2)}/mo
</span>
@@ -120,10 +120,10 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
</div>
{util ? (
<div className="mt-1.5 flex items-center gap-2">
<div className="h-1.5 flex-1 rounded-full bg-[color:var(--line)]">
<div className="h-1.5 flex-1 rounded-full bg-[color:var(--border)]">
<div className={`h-1.5 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} />
</div>
<span className="text-[10px] font-[family-name:var(--font-mono)] text-[color:var(--muted)]" style={{ fontVariantNumeric: "tabular-nums" }}>
<span className="text-[10px] font-[family-name:var(--font-mono)] text-muted-foreground" style={{ fontVariantNumeric: "tabular-nums" }}>
{Math.round(pct)}%
</span>
</div>
@@ -131,7 +131,7 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
</div>
<button
type="button"
className="shrink-0 rounded p-1 text-[color:var(--muted)] transition hover:text-[color:var(--danger)]"
className="shrink-0 rounded p-1 text-muted-foreground transition hover:text-destructive"
title="Delete policy"
aria-label="Delete policy"
onClick={(e) => {
@@ -20,12 +20,12 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
return (
<div className="grid gap-4">
<div><Input label={t("exec.secretNameLabel")} value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })) }} placeholder="e.g. ANTHROPIC_API_KEY" />{errors.name ? <div className="mt-1 text-[12px] text-[color:var(--danger)]" role="alert">{errors.name}</div> : null}</div>
<div><Input label={t("exec.secretNameLabel")} value={name} onChange={(v) => { setName(v); if (errors.name) setErrors((e) => ({ ...e, name: undefined })) }} placeholder="e.g. ANTHROPIC_API_KEY" />{errors.name ? <div className="mt-1 text-[12px] text-destructive" role="alert">{errors.name}</div> : null}</div>
<Input label="Description" value={description} onChange={setDescription} placeholder="What this secret is used for" />
<div><TextArea label="Secret value" value={value} onChange={(v) => { setValue(v); if (errors.value) setErrors((e) => ({ ...e, value: undefined })) }} rows={5} placeholder="Paste the secret value" />{errors.value ? <div className="mt-1 text-[12px] text-[color:var(--danger)]" role="alert">{errors.value}</div> : null}</div>
<div><TextArea label="Secret value" value={value} onChange={(v) => { setValue(v); if (errors.value) setErrors((e) => ({ ...e, value: undefined })) }} rows={5} placeholder="Paste the secret value" />{errors.value ? <div className="mt-1 text-[12px] text-destructive" role="alert">{errors.value}</div> : null}</div>
{submitError ? <InlineNotice message={submitError} /> : null}
<ActionButton label={t("exec.storeSecret")} tone="accent" onClick={() => void (async () => { if (!validate()) return; setSubmitError(null); setSubmitting(true); try { unwrap(await window.agentCompany.saveSecret({ companyId, name: name.trim(), description, value })); setName(""); setDescription(""); setValue(""); setErrors({}); await onSaved() } catch (e) { setSubmitError(describeActionError(e)) } finally { setSubmitting(false) } })()} icon={ShieldCheck} loading={submitting} />
<div className="border-t border-[color:var(--line)] pt-4 text-[12px] leading-relaxed text-[color:var(--muted)]">Connector secret bindings use `ENV_NAME=secret-name` lines. Values stay outside renderer state after submission.</div>
<div className="border-t border-border pt-4 text-[12px] leading-relaxed text-muted-foreground">Connector secret bindings use `ENV_NAME=secret-name` lines. Values stay outside renderer state after submission.</div>
</div>
)
}
@@ -29,21 +29,21 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
<ConfirmDialog open={confirmDeleteSecretId !== null} title="Delete secret" message={`Permanently delete secret "${secretToDelete?.name ?? ""}"? Any connectors referencing it will lose access.`} errorMessage={deleteSecretError} onConfirm={() => confirmDeleteSecretId && void handleDeleteSecret(confirmDeleteSecretId)} onCancel={() => { setDeleteSecretError(null); setConfirmDeleteSecretId(null) }} />
{actionError ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.profile")}</div><div className="border-y border-[color:var(--line)] py-4"><div className="mb-2 text-[11px] text-[color:var(--muted)]">{t("exec.profilePath")}</div><div className="mono truncate text-[11px] text-[color:var(--muted-strong)]">{snapshot.profilePath}</div></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.dataset.theme = next; root.style.colorScheme = next === "system" ? "light dark" : next; void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved()) }} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.profile")}</div><div className="border-y border-border py-4"><div className="mb-2 text-[11px] text-muted-foreground">{t("exec.profilePath")}</div><div className="font-mono truncate text-[11px] text-muted-foreground">{snapshot.profilePath}</div></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.preferences")}</div>
<Select label={t("settings.theme")} value={theme} onChange={(v) => { const next = v as ProfileSnapshot["theme"]; setTheme(next); const root = document.documentElement; root.className = next; root.style.colorScheme = next === "system" ? "light dark" : next; void window.agentCompany.updateSettings({ theme: next }).then(() => onSaved()) }} options={[{ value: "system", label: t("settings.system") }, { value: "dark", label: t("settings.dark") }, { value: "light", label: t("settings.light") }]} />
<div className="mt-3"><Select label={t("settings.language")} value={selectedLocale} onChange={(v) => setSelectedLocale(v as AppLocale)} options={APP_LOCALES.map((loc) => ({ value: loc, label: LOCALE_LABELS[loc] }))} /></div>
<div className="border-y border-[color:var(--line)] py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-[color:var(--muted-strong)]"><span>{t("exec.enableUpdater")}</span><input type="checkbox" checked={updatesEnabled} onChange={(e) => setUpdatesEnabled(e.target.checked)} /></label></div>
<div className="border-y border-border py-4 mt-3"><label className="flex items-center justify-between text-[13px] text-muted-foreground"><span>{t("exec.enableUpdater")}</span><input type="checkbox" checked={updatesEnabled} onChange={(e) => setUpdatesEnabled(e.target.checked)} /></label></div>
</div>
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; return (
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.companyAutonomy")}</div>
<div className="space-y-3">
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked, reviewDeliverables: company.reviewDeliverables })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.autoApproveDesc")}</div></div></label></div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={!company.reviewDeliverables} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: company.autoApproveHires, reviewDeliverables: !e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.reviewDeliverablesLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.reviewDeliverablesDesc")}</div></div></label></div>
<div className="rounded-lg border border-border px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked, reviewDeliverables: company.reviewDeliverables })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--primary)]" /><div><div className="text-[13px] font-medium text-foreground">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-muted-foreground">{t("exec.autoApproveDesc")}</div></div></label></div>
<div className="rounded-lg border border-border px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={!company.reviewDeliverables} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: company.autoApproveHires, reviewDeliverables: !e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--primary)]" /><div><div className="text-[13px] font-medium text-foreground">{t("exec.reviewDeliverablesLabel")}</div><div className="mt-0.5 text-[11px] text-muted-foreground">{t("exec.reviewDeliverablesDesc")}</div></div></label></div>
</div></div>) })()}
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackend")}</div><div className="border-y border-border py-3 text-[13px] text-muted-foreground">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-border overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-border"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-border last:border-b-0"><td className="px-4 py-3 font-medium text-foreground">{s.name}</td><td className="px-4 py-3 text-muted-foreground">{s.description || "\u2014"}</td><td className="px-4 py-3 font-mono text-muted-foreground">{s.backend}</td><td className="px-4 py-3 font-mono text-muted-foreground">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
</div>
)
}
@@ -73,7 +73,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
<div
role="region"
aria-label="Social post draft preview"
className="rounded-[8px] border px-4 py-3"
className="rounded-lg border px-4 py-3"
style={{ borderColor: `${platformColor}4D` }}
>
<div className="mb-2 flex items-center gap-2">
@@ -87,23 +87,23 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
<div className="mb-3 flex items-center gap-2">
<PlatformIcon size={16} style={{ color: platformColor }} />
<span className="text-[13px] font-medium text-[color:var(--text)]">
<span className="text-[13px] font-medium text-foreground">
{config.label}
</span>
{draft.socialAccountId ? (
<span className="text-[13px] font-[family-name:var(--font-mono)] text-[color:var(--muted)]">
<span className="text-[13px] font-[family-name:var(--font-mono)] text-muted-foreground">
@{draft.socialAccountId.slice(0, 8)}
</span>
) : null}
</div>
<div className="mb-2 max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[13px] leading-relaxed text-[color:var(--text)]">
<div className="mb-2 max-h-[200px] overflow-y-auto whitespace-pre-wrap text-[13px] leading-relaxed text-foreground">
{draft.content}
</div>
<div
className="mb-2 text-[10px] font-[family-name:var(--font-mono)]"
style={{ color: isOverLimit ? "var(--danger)" : "var(--muted)" }}
style={{ color: isOverLimit ? "var(--destructive)" : "var(--muted-foreground)" }}
aria-live="polite"
>
{charCount} / {charLimit}{isOverLimit ? " -- over limit" : ""}
@@ -112,7 +112,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
{draft.mediaUrls.length > 0 ? (
<div className="mb-2 space-y-1">
{draft.mediaUrls.map((url, idx) => (
<div key={idx} className="flex items-center gap-1.5 text-[11px] text-[color:var(--accent)]">
<div key={idx} className="flex items-center gap-1.5 text-[11px] text-primary">
<LinkIcon size={12} />
<span className="truncate max-w-[300px]">{url}</span>
</div>
@@ -121,7 +121,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
) : null}
{draft.scheduledAt ? (
<div className="flex items-center gap-1.5 text-[13px] font-[family-name:var(--font-mono)] text-[color:var(--warn)]">
<div className="flex items-center gap-1.5 text-[13px] font-[family-name:var(--font-mono)] text-yellow-600 dark:text-yellow-400">
<Clock size={14} />
Scheduled: {formatScheduledDate(draft.scheduledAt)}
</div>
@@ -26,25 +26,25 @@ export function TaskComments({ taskId, companyId, agents, comments, onRefresh }:
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Robot size={16} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("exec.communication")}</span>
<span className="text-[11px] text-[color:var(--muted)]">({comments.length})</span>
<Robot size={16} className="text-muted-foreground" />
<span className="text-[13px] font-semibold text-foreground">{t("exec.communication")}</span>
<span className="text-[11px] text-muted-foreground">({comments.length})</span>
</div>
{comments.length === 0 ? (
<div className="text-[12px] text-[color:var(--muted)]">{t("exec.noMessagesYet")}</div>
<div className="text-[12px] text-muted-foreground">{t("exec.noMessagesYet")}</div>
) : (
<div className="max-h-[240px] space-y-2 overflow-y-auto rounded-lg border border-[color:var(--line)] p-3">
<div className="max-h-[240px] space-y-2 overflow-y-auto rounded-lg border border-border p-3">
{comments.map((comment) => {
const isAgent = Boolean(comment.authorAgentId)
const agent = isAgent ? agents.find((a) => a.id === comment.authorAgentId) : null
return (
<div key={comment.id} className="rounded-md bg-[color:var(--panel-soft)] px-3 py-2">
<div key={comment.id} className="rounded-md bg-muted px-3 py-2">
<div className="flex items-center gap-2">
<span className={`text-[12px] font-semibold ${isAgent ? "text-[color:var(--accent)]" : "text-[color:var(--text)]"}`}>{comment.authorName}</span>
{agent && <span className="text-[10px] text-[color:var(--muted)]">{agent.role}</span>}
<span className="text-[10px] text-[color:var(--muted)]">{formatTime(comment.createdAt)}</span>
<span className={`text-[12px] font-semibold ${isAgent ? "text-primary" : "text-foreground"}`}>{comment.authorName}</span>
{agent && <span className="text-[10px] text-muted-foreground">{agent.role}</span>}
<span className="text-[10px] text-muted-foreground">{formatTime(comment.createdAt)}</span>
</div>
<div className="mt-1 whitespace-pre-wrap text-[13px] text-[color:var(--text)]">{comment.body}</div>
<div className="mt-1 whitespace-pre-wrap text-[13px] text-foreground">{comment.body}</div>
</div>
)
})}
@@ -52,7 +52,7 @@ export function TaskComments({ taskId, companyId, agents, comments, onRefresh }:
)}
{submitError ? <InlineNotice message={submitError} /> : null}
<div className="flex gap-2">
<input className="flex-1 rounded-md border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:border-[color:var(--accent)] focus:outline-none" placeholder={t("exec.sendMessage")} value={newComment} onChange={(e) => setNewComment(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void handleSubmit() } }} />
<input className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-[13px] text-foreground placeholder:text-muted-foreground focus:border-[color:var(--primary)] focus:outline-none" placeholder={t("exec.sendMessage")} value={newComment} onChange={(e) => setNewComment(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void handleSubmit() } }} />
<ActionButton label={t("exec.send")} tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
</div>
</div>
@@ -38,7 +38,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
return (
<div className="space-y-6">
<ConfirmDialog open={confirmDelete} title={t("exec.deleteTask")} message={`${t("exec.deleteTaskConfirm").replace("{0}", task.title)}`} errorMessage={deleteError} onConfirm={() => void handleDelete()} onCancel={() => { setDeleteError(null); setConfirmDelete(false) }} />
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-[8px] border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"}`}><Warning size={16} weight="fill" />{assignee.name} has used {Math.round(budgetWarning * 100)}% of their monthly budget ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
{budgetWarning > 0.8 && assignee ? <div className={`flex items-center gap-2 rounded-lg border px-4 py-3 text-[13px] font-medium ${budgetWarning > 1 ? "border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 text-destructive" : "border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"}`}><Warning size={16} weight="fill" />{assignee.name} has used {Math.round(budgetWarning * 100)}% of their monthly budget ({formatMoney(assignee.spentMonthlyUsd)} / {formatMoney(assignee.budgetMonthlyUsd)})</div> : null}
{actionError ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
{editing ? (
@@ -56,16 +56,16 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
) : (
<>
<div className="grid gap-x-6 gap-y-2 text-[13px] md:grid-cols-2">
{assignee && <div className="text-[color:var(--muted)]">Assignee: <span className="text-[color:var(--text)]">{assignee.name}</span></div>}
{project && <div className="text-[color:var(--muted)]">Project: <span className="text-[color:var(--text)]">{project.name}</span></div>}
{goal && <div className="text-[color:var(--muted)]">Goal: <span className="text-[color:var(--text)]">{goal.title}</span></div>}
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-[color:var(--muted)]">Parent: <span className="text-[color:var(--text)]">{parent.title}</span></div> : null })()}
{workspace && <div className="text-[color:var(--muted)]">Workspace: <span className="text-[color:var(--text)]">{workspace.name}</span></div>}
{task.costUsd > 0 && <div className="text-[color:var(--muted)]">Cost: <span className="mono tabular-nums text-[color:var(--text)]">{formatMoney(task.costUsd)}</span></div>}
{assignee && <div className="text-muted-foreground">Assignee: <span className="text-foreground">{assignee.name}</span></div>}
{project && <div className="text-muted-foreground">Project: <span className="text-foreground">{project.name}</span></div>}
{goal && <div className="text-muted-foreground">Goal: <span className="text-foreground">{goal.title}</span></div>}
{task.parentId && (() => { const parent = tasks.find((t) => t.id === task.parentId); return parent ? <div className="text-muted-foreground">Parent: <span className="text-foreground">{parent.title}</span></div> : null })()}
{workspace && <div className="text-muted-foreground">Workspace: <span className="text-foreground">{workspace.name}</span></div>}
{task.costUsd > 0 && <div className="text-muted-foreground">Cost: <span className="font-mono tabular-nums text-foreground">{formatMoney(task.costUsd)}</span></div>}
</div>
{task.description ? <div className="text-[13px] leading-relaxed text-[color:var(--muted-strong)] whitespace-pre-wrap">{task.description}</div> : null}
{task.status === "in_review" && reviewer ? <div className="rounded-[8px] border border-[color:var(--accent-soft)] bg-[color:var(--accent-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--accent-text)]"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--accent-text)]">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
: needsGovernance ? <div className="rounded-[8px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-[color:var(--warn)]"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-[color:var(--muted-strong)]">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
{task.description ? <div className="text-[13px] leading-relaxed text-muted-foreground whitespace-pre-wrap">{task.description}</div> : null}
{task.status === "in_review" && reviewer ? <div className="rounded-lg border border-[color:var(--primary)] bg-accent px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-primary"><Robot size={16} weight="fill" />Being reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""}</div><div className="mt-1 text-[13px] leading-relaxed text-primary">{assignee?.name} submitted this task for review. {reviewer.name} will approve, request revisions, or escalate.</div></div>
: needsGovernance ? <div className="rounded-lg border border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 px-4 py-3"><div className="flex items-center gap-2 text-[13px] font-semibold text-yellow-600 dark:text-yellow-400"><ShieldCheck size={16} weight="fill" />{task.status === "blocked" ? t("exec.governanceRequired") : t("exec.waitingOnReview")}</div><div className="mt-1 text-[13px] leading-relaxed text-muted-foreground">{task.status === "blocked" ? t("exec.blockedGovernance") : t("exec.reviewGovernance")}</div><div className="mt-3"><ActionButton label={task.status === "blocked" ? t("exec.createUnblockApproval") : t("exec.createReviewApproval")} onClick={() => void handleCreateApproval()} icon={ShieldCheck} loading={creatingApproval} /></div></div> : null}
<div className="flex items-center gap-3">
{activeRun ? <ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" /> : <>
{["todo","in_progress","blocked"].includes(task.status) && <ActionButton label={t("exec.run")} onClick={() => void onRun(task.id)} icon={Play} tone="accent" />}
@@ -80,20 +80,20 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
)}
{(() => { const subtasks = tasks.filter((t) => t.parentId === task.id); if (subtasks.length === 0) return null; return (
<div className="space-y-2">
<div className="flex items-center gap-2"><ListChecks size={14} className="text-[color:var(--muted)]" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-[color:var(--success)]">{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done</span></div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]"><div className="h-full rounded-full bg-[color:var(--success)] transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
<div className="flex items-center gap-2"><ListChecks size={14} className="text-muted-foreground" /><span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.subtasks")} ({subtasks.length})</span><span className="text-[10px] text-green-600 dark:text-green-400">{subtasks.filter((t) => t.status === "done").length}/{subtasks.length} done</span></div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"><div className="h-full rounded-full bg-green-600 dark:bg-green-400 transition-all duration-300" style={{ width: `${subtasks.length > 0 ? (subtasks.filter((t) => t.status === "done").length / subtasks.length) * 100 : 0}%` }} /></div>
<div className="space-y-1">{subtasks.map((sub) => { const subAssignee = agentMap.get(sub.assigneeAgentId ?? ""); return (
<div key={sub.id} className="flex items-center gap-2 rounded-[6px] px-3 py-2 text-[12px] bg-[color:var(--panel-soft)]">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${sub.status === "done" ? "bg-[color:var(--success)]" : sub.status === "in_progress" ? "bg-[color:var(--accent)]" : sub.status === "blocked" ? "bg-[color:var(--danger)]" : sub.status === "in_review" ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)]"}`} />
<span className={`flex-1 truncate ${sub.status === "done" ? "line-through text-[color:var(--muted)]" : "text-[color:var(--text)]"}`}>{sub.title}</span>
{subAssignee ? <span className="shrink-0 text-[10px] text-[color:var(--muted)]">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
<div key={sub.id} className="flex items-center gap-2 rounded-lg px-3 py-2 text-[12px] bg-muted">
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${sub.status === "done" ? "bg-green-600 dark:bg-green-400" : sub.status === "in_progress" ? "bg-primary" : sub.status === "blocked" ? "bg-destructive" : sub.status === "in_review" ? "bg-yellow-600 dark:bg-yellow-400" : "bg-[color:var(--muted-foreground)]"}`} />
<span className={`flex-1 truncate ${sub.status === "done" ? "line-through text-muted-foreground" : "text-foreground"}`}>{sub.title}</span>
{subAssignee ? <span className="shrink-0 text-[10px] text-muted-foreground">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
</div>) })}</div></div>) })()}
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} />
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
<div key={run.id} className={`rounded-[8px] border px-3 py-2.5 ${isActive ? "border-[color:var(--success)] bg-[color:var(--success-soft)]" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--danger)] border-opacity-30 bg-[color:var(--danger-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)]"}`}>
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--success)]" /> : null}<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-[color:var(--muted)]">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
{run.errorMessage ? <div className="mt-1.5 rounded bg-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)] break-words">{run.errorMessage}</div> : null}
</div>) })}{runs.length > 8 && <div className="text-[11px] text-[color:var(--muted)] text-center">+{runs.length - 8} more runs</div>}</div></div>}
{runs.length > 0 && <div className="space-y-2"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("exec.runHistory")}</div><div className="space-y-2">{runs.slice(0, 8).map((run) => { const runAgent = agentMap.get(run.agentId); const isActive = run.status === "running" || run.status === "queued"; return (
<div key={run.id} className={`rounded-lg border px-3 py-2.5 ${isActive ? "border-[color:var(--primary)] bg-green-100 dark:bg-green-900/50" : run.status === "failed" || run.status === "timed_out" ? "border-[color:var(--destructive)] border-opacity-30 bg-red-100 dark:bg-red-900/50" : "border-border bg-muted"}`}>
<div className="flex items-center justify-between gap-2"><div className="min-w-0 flex-1"><div className="flex items-center gap-2">{isActive ? <span className="h-2 w-2 animate-pulse rounded-full bg-green-600 dark:bg-green-400" /> : null}<span className="truncate text-[13px] font-medium text-foreground">{run.summary || (isActive ? "Running..." : "Completed run")}</span></div><div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">{runAgent ? <span>{runAgent.name}</span> : null}<span>{run.connectorId.replace("_local", "")}</span>{run.model ? <span className="font-mono">{run.model}</span> : null}<span>{formatTime(run.startedAt)}</span>{run.costUsd != null && run.costUsd > 0 ? <span className="font-mono tabular-nums">{formatMoney(run.costUsd)}</span> : null}</div></div><StatusPill status={run.status} /></div>
{run.errorMessage ? <div className="mt-1.5 rounded bg-red-100 dark:bg-red-900/50 px-2 py-1 text-[11px] text-destructive break-words">{run.errorMessage}</div> : null}
</div>) })}{runs.length > 8 && <div className="text-[11px] text-muted-foreground text-center">+{runs.length - 8} more runs</div>}</div></div>}
</div>
)
}
@@ -22,7 +22,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
return (
<div
ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]"
className="h-full overflow-auto border-y border-border"
role="listbox"
aria-label="Task queue"
onKeyDown={handleKeyDown}
@@ -52,18 +52,18 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<button
className={`relative w-full border-b border-[color:var(--line)] px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedTaskId === task.id ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "bg-transparent hover:bg-[color:var(--panel-soft)]"}`}
className={`relative w-full border-b border-border px-4 py-3 text-left transition duration-150 last:border-b-0 ${selectedTaskId === task.id ? "bg-accent border-l-[3px] border-l-[color:var(--primary)]" : "bg-transparent hover:bg-muted"}`}
onClick={() => onSelect(task.id)}
tabIndex={-1}
>
{selectedTaskId === task.id ? <span className="absolute inset-y-3 left-0 w-0.5 rounded-full bg-[color:var(--accent)]" /> : null}
{selectedTaskId === task.id ? <span className="absolute inset-y-3 left-0 w-0.5 rounded-full bg-primary" /> : null}
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2.5">
<PriorityDot priority={task.priority} />
<div className={`truncate text-[13px] text-[color:var(--text)] ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
<div className={`truncate text-[13px] text-foreground ${selectedTaskId === task.id ? "font-semibold" : "font-medium"}`}>{task.title}</div>
</div>
<StatusPill status={activeRun?.status ?? task.status} />
</div>
@@ -15,18 +15,18 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents,
return (
<div className="space-y-5">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<div className="flex items-start justify-between gap-3"><div className="min-w-0"><div className="flex items-center gap-2"><div className="text-[15px] font-semibold text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div><div className="mono mt-1 text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div><div className="mt-2 text-[12px] text-[color:var(--muted)]">{project ? <>Project: <span className="text-[color:var(--text)]">{project.name}</span></> : "No linked project"}{workspace.repoUrl ? <span> · Repo: <span className="text-[color:var(--text)]">{workspace.repoUrl}</span></span> : null}{workspace.repoRef ? <span> · Ref: <span className="text-[color:var(--text)]">{workspace.repoRef}</span></span> : null}</div></div><ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} /></div>
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex items-start justify-between gap-3"><div className="min-w-0"><div className="flex items-center gap-2"><div className="text-[15px] font-semibold text-foreground">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div><div className="font-mono mt-1 text-[11px] text-muted-foreground">{workspace.localPath}</div><div className="mt-2 text-[12px] text-muted-foreground">{project ? <>Project: <span className="text-foreground">{project.name}</span></> : "No linked project"}{workspace.repoUrl ? <span> · Repo: <span className="text-foreground">{workspace.repoUrl}</span></span> : null}{workspace.repoRef ? <span> · Ref: <span className="text-foreground">{workspace.repoRef}</span></span> : null}</div></div><ActionButton label={t("exec.openFolder")} onClick={() => void onOpen(workspace.localPath)} icon={ArrowSquareOut} /></div>
</div>
<div className="grid gap-3 md:grid-cols-4">
{[{ label: t("exec.agents"), val: wAgents.length }, { label: t("exec.tasks"), val: wTasks.length }, { label: t("exec.runs"), val: wRuns.length }, { label: t("exec.docs"), val: wDocs.length }].map((s) => (
<div key={s.label} className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-[color:var(--text)]">{s.val}</div></div>
<div key={s.label} className="rounded-lg border border-border bg-card p-4"><div className="text-[11px] uppercase tracking-[0.08em] text-muted-foreground">{s.label}</div><div className="mt-2 text-[24px] font-semibold text-foreground">{s.val}</div></div>
))}
</div>
<div className="grid gap-4 xl:grid-cols-3">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div><div className="text-[12px] font-medium text-[color:var(--text)]">{a.name}</div><div className="text-[11px] text-[color:var(--muted)]">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-[color:var(--text)]">{tk.title}</span></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"><div className="mb-3 text-[12px] font-semibold text-[color:var(--text)]">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-[color:var(--muted)]">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-[8px] bg-[color:var(--panel-soft)] px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-[color:var(--text)]">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-[color:var(--muted)]">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
<div className="rounded-lg border border-border bg-card p-4"><div className="mb-3 text-[12px] font-semibold text-foreground">{t("exec.assignedAgents")}</div>{wAgents.length === 0 ? <div className="text-[12px] text-muted-foreground">{t("exec.noAgentsBound")}</div> : <div className="space-y-2">{wAgents.map((a) => <div key={a.id} className="flex items-center justify-between rounded-lg bg-muted px-3 py-2"><div><div className="text-[12px] font-medium text-foreground">{a.name}</div><div className="text-[11px] text-muted-foreground">{a.role}</div></div><StatusPill status={a.status} /></div>)}</div>}</div>
<div className="rounded-lg border border-border bg-card p-4"><div className="mb-3 text-[12px] font-semibold text-foreground">{t("exec.openTasks")}</div>{wTasks.length === 0 ? <div className="text-[12px] text-muted-foreground">{t("exec.noTasksTarget")}</div> : <div className="space-y-2">{wTasks.slice(0, 6).map((tk) => <div key={tk.id} className="rounded-lg bg-muted px-3 py-2"><div className="flex items-center gap-2"><PriorityDot priority={tk.priority} /><span className="text-[12px] font-medium text-foreground">{tk.title}</span></div><div className="mt-1 text-[11px] text-muted-foreground">{tk.status.replaceAll("_"," ")}</div></div>)}</div>}</div>
<div className="rounded-lg border border-border bg-card p-4"><div className="mb-3 text-[12px] font-semibold text-foreground">{t("exec.recentRuns")}</div>{wRuns.length === 0 ? <div className="text-[12px] text-muted-foreground">{t("exec.noRunsExecuted")}</div> : <div className="space-y-2">{wRuns.slice(0, 6).map((r) => <div key={r.id} className="rounded-lg bg-muted px-3 py-2"><div className="flex items-center justify-between gap-2"><span className="text-[12px] font-medium text-foreground">{r.summary || `Run ${r.id.slice(0, 8)}`}</span><StatusPill status={r.status} /></div><div className="mt-1 text-[11px] text-muted-foreground">{formatTime(r.startedAt)} → {formatTime(r.finishedAt)}</div></div>)}</div>}</div>
</div>
</div>
)
@@ -24,11 +24,11 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
const linkedProject = projects.find((p) => p.id === workspace.projectId) ?? null
return (
<ListRow key={workspace.id}>
<div className={`mb-2 rounded-[8px] p-1 ${selectedWorkspaceId === workspace.id ? "bg-[color:var(--accent-soft)]" : ""}`}>
<div className={`mb-2 rounded-lg p-1 ${selectedWorkspaceId === workspace.id ? "bg-accent" : ""}`}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2"><div className="truncate text-[13px] text-[color:var(--text)]">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
<div className="mono truncate text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div>
<div className="flex items-center gap-2"><div className="truncate text-[13px] text-foreground">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
<div className="font-mono truncate text-[11px] text-muted-foreground">{workspace.localPath}</div>
</div>
<div className="flex items-center gap-2">
{workspace.isPrimary ? <StatusPill status="ready" /> : null}
@@ -37,7 +37,7 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
<ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
</div>
</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-[color:var(--text)]">{linkedProject.name}</span></span> : null}</div>
<div className="truncate text-[11px] text-muted-foreground">{workspace.repoUrl || "No remote URL"} {workspace.repoRef ? `· ${workspace.repoRef}` : ""}{linkedProject ? <span> · Project: <span className="text-foreground">{linkedProject.name}</span></span> : null}</div>
</div>
</ListRow>
)
@@ -27,11 +27,11 @@ export const ENTITY_TYPE_OPTIONS = [
export function WorkspaceStatePill({ state }: { state: string }) {
const colorMap: Record<string, string> = {
ready: "text-[color:var(--success)] bg-[color:var(--success-soft)]",
busy: "text-[color:var(--warn)] bg-[color:var(--warn-soft)]",
blocked: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]",
missing: "text-[color:var(--muted-strong)] bg-[color:var(--panel-soft)]",
error: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]",
ready: "text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/50",
busy: "text-yellow-600 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/50",
blocked: "text-destructive bg-red-100 dark:bg-red-900/50",
missing: "text-muted-foreground bg-muted",
error: "text-destructive bg-red-100 dark:bg-red-900/50",
}
return (
<span className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${colorMap[state] ?? colorMap.missing}`}>
@@ -41,6 +41,6 @@ export function WorkspaceStatePill({ state }: { state: string }) {
}
export function PriorityDot({ priority }: { priority: string }) {
const color = priority === "critical" ? "bg-[color:var(--danger)]" : priority === "high" ? "bg-[color:var(--warn)]" : priority === "low" ? "bg-[color:var(--muted)]" : "bg-[color:var(--muted-strong)]"
const color = priority === "critical" ? "bg-destructive" : priority === "high" ? "bg-yellow-600 dark:bg-yellow-400" : priority === "low" ? "bg-[color:var(--muted-foreground)]" : "bg-[color:var(--muted-foreground)]"
return <span className={`inline-block h-2 w-2 shrink-0 rounded-full ${color}`} aria-label={`${priority} priority`} />
}
@@ -87,16 +87,16 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<button
onClick={() => setFilterStage(filterStage === stage ? "all" : stage)}
className={`px-3 py-2 rounded-md text-xs font-medium transition-colors whitespace-nowrap ${
filterStage === stage ? "ring-1 ring-[color:var(--accent)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
filterStage === stage ? "ring-1 ring-[color:var(--primary)]/40 bg-muted" : "hover:bg-muted"
} ${stageColor(stage)}`}
>
{stageLabelMap[stage]}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--border)] text-[10px]">
{stageCounts[stage]}
</span>
</button>
{idx < STAGE_ORDER.length - 2 && (
<ArrowRight size={12} className="text-[color:var(--muted)] flex-shrink-0" />
<ArrowRight size={12} className="text-muted-foreground flex-shrink-0" />
)}
</div>
))}
@@ -104,11 +104,11 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<button
onClick={() => setFilterStage(filterStage === "rejected" ? "all" : "rejected")}
className={`ml-4 px-3 py-2 rounded-md text-xs font-medium transition-colors ${
filterStage === "rejected" ? "ring-1 ring-[color:var(--danger)]/40 bg-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]"
filterStage === "rejected" ? "ring-1 ring-[color:var(--destructive)]/40 bg-muted" : "hover:bg-muted"
} ${stageColor("rejected")}`}
>
{stageLabelMap.rejected}
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--line)] text-[10px]">
<span className="ml-1.5 inline-flex items-center justify-center h-5 w-5 rounded-full bg-[color:var(--border)] text-[10px]">
{stageCounts.rejected}
</span>
</button>
@@ -118,7 +118,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
{pendingCount > 0 && (
<Panel title={t("hiring.pendingApproval")}>
<div className="divide-y divide-[color:var(--line)]">
<div className="divide-y divide-[color:var(--border)]">
{allCandidates
.filter(c => c.stage === "requisition" || c.stage === "pending_approval")
.map((candidate) => {
@@ -126,21 +126,21 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
const ag = candidate.agent;
return (
<div key={a?.id ?? ag?.id} className="px-4 py-3 flex items-start gap-3">
<div className="flex-shrink-0 mt-0.5 h-8 w-8 rounded-full bg-[color:var(--warn-soft)] flex items-center justify-center">
<UserPlus size={16} className="text-[color:var(--warn)]" />
<div className="flex-shrink-0 mt-0.5 h-8 w-8 rounded-full bg-yellow-100 dark:bg-yellow-900/50 flex items-center justify-center">
<UserPlus size={16} className="text-yellow-600 dark:text-yellow-400" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)]">{ag?.name ?? t("hiring.pendingHires")}</span>
<span className="font-medium text-sm text-foreground">{ag?.name ?? t("hiring.pendingHires")}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabelMap[candidate.stage]}
</span>
</div>
<div className="text-xs text-[color:var(--muted)] mt-0.5">
<div className="text-xs text-muted-foreground mt-0.5">
{ag ? `${ag.role} — ${ag.title}` : a?.payloadSummary}
</div>
{ag?.capabilities && (
<div className="text-xs text-[color:var(--muted)] mt-1 line-clamp-2">{ag.capabilities}</div>
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">{ag.capabilities}</div>
)}
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
@@ -195,7 +195,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
{filteredCandidates.length === 0 ? (
<EmptyState title={t("panel.noData")} />
) : (
<div className="divide-y divide-[color:var(--line)]">
<div className="divide-y divide-[color:var(--border)]">
{filteredCandidates.map((candidate) => {
const ag = candidate.agent;
if (!ag) return null;
@@ -206,32 +206,32 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
return (
<div
key={ag.id}
className="px-4 py-3 flex items-center gap-3 hover:bg-[color:var(--panel-soft)] cursor-pointer transition-colors"
className="px-4 py-3 flex items-center gap-3 hover:bg-muted cursor-pointer transition-colors"
onClick={() => onNavigateToAgent(ag.id)}
>
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-[color:var(--panel-soft)] flex items-center justify-center text-xs font-bold text-[color:var(--muted-strong)]">
<div className="flex-shrink-0 h-8 w-8 rounded-full bg-muted flex items-center justify-center text-xs font-bold text-muted-foreground">
{getAgentInitials(ag.name)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-[color:var(--text)] truncate">{ag.name}</span>
<span className="font-medium text-sm text-foreground truncate">{ag.name}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabelMap[candidate.stage]}
</span>
<StatusPill status={ag.status} />
</div>
<div className="text-xs text-[color:var(--muted)] mt-0.5">
<div className="text-xs text-muted-foreground mt-0.5">
{ag.role} — {ag.title}{ag.department ? ` | ${ag.department}` : ""}
</div>
</div>
<div className="flex-shrink-0 text-right">
<div className="text-xs text-[color:var(--muted)]">
<div className="text-xs text-muted-foreground">
{manager ? `${t("hiring.reportsTo")} ${manager.name}` : t("hiring.topLevel")}
</div>
{requestedBy && (
<div className="text-[10px] text-[color:var(--muted)]">{t("hiring.proposedBy")} {requestedBy.name}</div>
<div className="text-[10px] text-muted-foreground">{t("hiring.proposedBy")} {requestedBy.name}</div>
)}
<div className="text-xs text-[color:var(--muted)]">${ag.budgetMonthlyUsd}/mo</div>
<div className="text-xs text-muted-foreground">${ag.budgetMonthlyUsd}/mo</div>
</div>
</div>
);
@@ -241,23 +241,23 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</Panel>
<div className="grid grid-cols-4 gap-3">
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.totalAgents")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">{agents.filter(a => a.status !== "terminated").length}</div>
<div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.totalAgents")}</div>
<div className="text-2xl font-bold text-foreground mt-1">{agents.filter(a => a.status !== "terminated").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.pendingHires")}</div>
<div className="text-2xl font-bold text-[color:var(--warn)] mt-1">{agents.filter(a => a.status === "pending_approval").length}</div>
<div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.pendingHires")}</div>
<div className="text-2xl font-bold text-yellow-600 dark:text-yellow-400 mt-1">{agents.filter(a => a.status === "pending_approval").length}</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.departments")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
<div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.departments")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
{new Set(agents.filter(a => a.department && a.status !== "terminated").map(a => a.department)).size}
</div>
</div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.monthlyPayroll")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1">
<div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.monthlyPayroll")}</div>
<div className="text-2xl font-bold text-foreground mt-1">
${agents.filter(a => a.status !== "terminated" && a.status !== "pending_approval").reduce((s, a) => s + a.budgetMonthlyUsd, 0).toLocaleString()}
</div>
</div>
@@ -271,15 +271,15 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
const active = deptAgents.filter(a => a.status !== "pending_approval").length;
if (active === 0 && pending === 0) return null;
return (
<div key={dept} className="bg-[color:var(--panel-soft)] rounded-md px-3 py-2">
<div className="text-xs font-medium text-[color:var(--text)] capitalize">{dept.replace("_", " ")}</div>
<div key={dept} className="bg-muted rounded-md px-3 py-2">
<div className="text-xs font-medium text-foreground capitalize">{dept.replace("_", " ")}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-sm font-bold text-[color:var(--text)]">{active}</span>
<span className="text-[10px] text-[color:var(--muted)]">{t("hiring.active")}</span>
<span className="text-sm font-bold text-foreground">{active}</span>
<span className="text-[10px] text-muted-foreground">{t("hiring.active")}</span>
{pending > 0 && (
<>
<span className="text-sm font-bold text-[color:var(--warn)]">+{pending}</span>
<span className="text-[10px] text-[color:var(--warn)]">{t("hiring.pendingApproval")}</span>
<span className="text-sm font-bold text-yellow-600 dark:text-yellow-400">+{pending}</span>
<span className="text-[10px] text-yellow-600 dark:text-yellow-400">{t("hiring.pendingApproval")}</span>
</>
)}
</div>
@@ -24,12 +24,12 @@ export function getHiringStage(agent: AgentRecord, approval: ApprovalRecord | nu
export function stageColor(stage: HiringStage): string {
switch (stage) {
case "requisition": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]";
case "pending_approval": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]";
case "approved": return "bg-[color:var(--success-soft)] text-[color:var(--success)]";
case "onboarding": return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]";
case "active": return "bg-[color:var(--success-soft)] text-[color:var(--success)]";
case "rejected": return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]";
case "requisition": return "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400";
case "pending_approval": return "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400";
case "approved": return "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400";
case "onboarding": return "bg-accent text-primary";
case "active": return "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400";
case "rejected": return "bg-red-100 dark:bg-red-900/50 text-destructive";
}
}
+16 -16
View File
@@ -61,12 +61,12 @@ export function Inbox({ items, onNavigate }: InboxProps) {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Bell size={20} weight="fill" className="text-primary" />
</div>
<div>
<h2 className="text-[18px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
<h2 className="text-[18px] font-bold tracking-[-0.02em] text-foreground">{t("inbox.title")}</h2>
<p className="text-[12px] text-muted-foreground">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p>
</div>
@@ -81,14 +81,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
onClick={() => setFilter(btn.key)}
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
filter === btn.key
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"
}`}
>
{btn.label}
{btn.count > 0 ? (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${
filter === btn.key ? "bg-[color:var(--text-on-accent)]/20 text-[color:var(--text-on-accent)]" : "bg-[color:var(--line)] text-[color:var(--muted)]"
filter === btn.key ? "bg-[color:var(--primary-foreground)]/20 text-primary-foreground" : "bg-[color:var(--border)] text-muted-foreground"
}`}>
{btn.count}
</span>
@@ -98,10 +98,10 @@ export function Inbox({ items, onNavigate }: InboxProps) {
</div>
{filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Tray size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<Tray size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-muted-foreground">{t("inbox.noItemsNow")}</div>
</div>
) : (
<div className="space-y-1.5">
@@ -115,10 +115,10 @@ export function Inbox({ items, onNavigate }: InboxProps) {
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 20 }}
className={`group flex items-center gap-3 rounded-[8px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px ${severityColors[item.severity] ?? ""}`}
className={`group flex items-center gap-3 rounded-lg border border-l-[3px] border-border bg-card px-4 py-3 transition duration-150 hover:border-border hover:shadow-md hover:-translate-y-px ${severityColors[item.severity] ?? ""}`}
>
<div
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[8px]"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: config.bg }}
>
<Icon size={18} weight="fill" style={{ color: config.color }} />
@@ -129,7 +129,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
className="min-w-0 flex-1 text-left"
>
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{item.title}</span>
<span className="truncate text-[13px] font-medium text-foreground">{item.title}</span>
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: config.bg, color: config.color }}
@@ -137,7 +137,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
{config.label}
</span>
</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
{item.agentName ? (
<span className="flex items-center gap-1">
<Robot size={10} />
@@ -148,7 +148,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
<span className="shrink-0 tabular-nums">{timeAgo(item.createdAt)}</span>
</div>
</button>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)] opacity-0 transition group-hover:opacity-100 group-focus-within:opacity-100" />
<ArrowRight size={14} className="shrink-0 text-muted-foreground opacity-0 transition group-hover:opacity-100 group-focus-within:opacity-100" />
</motion.div>
);
})}
@@ -13,10 +13,10 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT();
if (!metrics) {
return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Target size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<Target size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-muted-foreground">{t("perf.noMetricsDesc")}</div>
</div>
);
}
@@ -48,8 +48,8 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
/>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div>
<div className="rounded-lg border border-border bg-card p-5">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Run Activity (30d)</div>
<div className="flex items-center gap-6">
<div className="flex-1">
<RunProgressBar
@@ -60,29 +60,29 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
</div>
<div className="flex gap-4 text-[12px]">
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
<span className="text-[color:var(--muted)]">{metrics.successfulRuns} succeeded</span>
<span className="h-2.5 w-2.5 rounded-full bg-green-600 dark:bg-green-400" />
<span className="text-muted-foreground">{metrics.successfulRuns} succeeded</span>
</span>
<span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--danger)]" />
<span className="text-[color:var(--muted)]">{metrics.failedRuns} failed</span>
<span className="h-2.5 w-2.5 rounded-full bg-destructive" />
<span className="text-muted-foreground">{metrics.failedRuns} failed</span>
</span>
</div>
</div>
</div>
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div>
<div className="overflow-x-auto rounded-[8px] border border-[color:var(--line)]">
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Agent Performance</div>
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-left">
<thead>
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Agent</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Tasks Done</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Success %</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Runs</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Avg Duration</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)] text-right">Cost</th>
<tr className="border-b border-border bg-muted">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground">Agent</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Tasks Done</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Success %</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Runs</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Avg Duration</th>
<th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Cost</th>
</tr>
</thead>
<tbody>
@@ -91,30 +91,30 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
.map((am) => (
<tr
key={am.agentId}
className="border-b border-[color:var(--line)] bg-[color:var(--panel)] transition hover:bg-[color:var(--panel-soft)] cursor-pointer"
className="border-b border-border bg-card transition hover:bg-muted cursor-pointer"
onClick={() => onNavigate("agents", am.agentId)}
>
<td className="px-4 py-2.5">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-[color:var(--panel-soft)] text-[10px] font-bold text-[color:var(--muted-strong)]">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground">
{getAgentInitials(am.agentName)}
</div>
<span className="text-[12px] font-medium text-[color:var(--text)]">{am.agentName}</span>
<span className="text-[12px] font-medium text-foreground">{am.agentName}</span>
</div>
</td>
<td className="px-3 py-2.5 text-right text-[12px] font-semibold text-[color:var(--success)]">{am.tasksCompleted}</td>
<td className="px-3 py-2.5 text-right text-[12px] font-semibold text-green-600 dark:text-green-400">{am.tasksCompleted}</td>
<td className="px-3 py-2.5 text-right">
<span className={`text-[12px] font-semibold ${
am.successRate >= 80 ? "text-[color:var(--success)]"
: am.successRate >= 50 ? "text-[color:var(--warn)]"
: "text-[color:var(--danger)]"
am.successRate >= 80 ? "text-green-600 dark:text-green-400"
: am.successRate >= 50 ? "text-yellow-600 dark:text-yellow-400"
: "text-destructive"
}`}>
{am.successRate}%
</span>
</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{am.runsTotal}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatDuration(am.avgRunDurationSec)}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-[color:var(--muted)]">{formatMoney(am.totalCostUsd)}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-muted-foreground">{am.runsTotal}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-muted-foreground">{formatDuration(am.avgRunDurationSec)}</td>
<td className="px-3 py-2.5 text-right text-[12px] text-muted-foreground">{formatMoney(am.totalCostUsd)}</td>
</tr>
))}
</tbody>
+40 -40
View File
@@ -18,60 +18,60 @@ export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT();
if (!report) {
return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Clock size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div>
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<Clock size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-muted-foreground">{t("standup.noDataDesc")}</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<div className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Heartbeat size={20} weight="fill" className="text-primary" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
<h2 className="text-[16px] font-bold text-foreground">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-muted-foreground">
Generated {new Date(report.generatedAt).toLocaleString()}
</p>
</div>
</div>
<div className="text-[14px] font-medium text-[color:var(--text)]">{report.summary}</div>
<div className="text-[14px] font-medium text-foreground">{report.summary}</div>
<div className="mt-3 flex gap-4">
<div className="text-center">
<div className="text-[18px] font-bold tabular-nums text-[color:var(--success)]">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.productive")}</div>
<div className="text-[18px] font-bold tabular-nums text-green-600 dark:text-green-400">{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}</div>
<div className="text-[10px] text-muted-foreground">{t("standup.productive")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="h-10 w-px bg-[color:var(--border)]" />
<div className="text-center">
<div className="text-[18px] font-bold tabular-nums text-[color:var(--danger)]">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div>
<div className="text-[18px] font-bold tabular-nums text-destructive">{report.blockers.length}</div>
<div className="text-[10px] text-muted-foreground">{t("standup.blockers")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="h-10 w-px bg-[color:var(--border)]" />
<div className="text-center">
<div className="text-[18px] font-bold tabular-nums text-[color:var(--warn)]">{report.pendingApprovals}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.pendingApprovals")}</div>
<div className="text-[18px] font-bold tabular-nums text-yellow-600 dark:text-yellow-400">{report.pendingApprovals}</div>
<div className="text-[10px] text-muted-foreground">{t("standup.pendingApprovals")}</div>
</div>
<div className="h-10 w-px bg-[color:var(--line)]" />
<div className="h-10 w-px bg-[color:var(--border)]" />
<div className="text-center">
<div className={`text-[18px] font-bold tabular-nums ${report.budgetUtilization > 80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
<div className={`text-[18px] font-bold tabular-nums ${report.budgetUtilization > 80 ? "text-destructive" : "text-foreground"}`}>
{report.budgetUtilization}%
</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.budgetUsed")}</div>
<div className="text-[10px] text-muted-foreground">{t("standup.budgetUsed")}</div>
</div>
</div>
</div>
{report.companyHighlights.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.highlights")}</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("standup.highlights")}</div>
<div className="space-y-1.5">
{report.companyHighlights.map((h, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--success-soft)] px-3 py-2 text-[12px] text-[color:var(--success)]">
<div key={i} className="flex items-center gap-2 rounded-lg bg-green-100 dark:bg-green-900/50 px-3 py-2 text-[12px] text-green-600 dark:text-green-400">
<CheckCircle size={14} weight="fill" />
{h}
</div>
@@ -82,10 +82,10 @@ export function StandupView({ report, onNavigate }: StandupProps) {
{report.blockers.length > 0 ? (
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--danger)]">Blockers</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-destructive">Blockers</div>
<div className="space-y-1.5">
{report.blockers.map((b, i) => (
<div key={i} className="flex items-center gap-2 rounded-[6px] bg-[color:var(--danger-soft)] px-3 py-2 text-[12px] text-[color:var(--danger)]">
<div key={i} className="flex items-center gap-2 rounded-lg bg-red-100 dark:bg-red-900/50 px-3 py-2 text-[12px] text-destructive">
<Warning size={14} weight="fill" />
{b}
</div>
@@ -95,46 +95,46 @@ export function StandupView({ report, onNavigate }: StandupProps) {
) : null}
<section>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("standup.agentStatus")}</div>
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{t("standup.agentStatus")}</div>
<div className="space-y-2">
{report.agentReports.map((ar) => (
<button
key={ar.agentId}
type="button"
onClick={() => onNavigate("agents", ar.agentId)}
className="flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
className="flex w-full items-center gap-3 rounded-lg border border-border bg-card px-4 py-3 text-left transition hover:border-border hover:shadow-sm"
>
<div className="relative">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[12px] font-bold ${
ar.status === "running" ? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: ar.status === "error" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
ar.status === "running" ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: ar.status === "error" ? "bg-red-100 dark:bg-red-900/50 text-destructive"
: "bg-muted text-muted-foreground"
}`}>
{getAgentInitials(ar.agentName)}
</div>
{ar.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3">
<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 w-2 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 w-2 rounded-full bg-green-600 dark:bg-green-400" />
</span>
) : null}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[color:var(--text)]">{ar.agentName}</span>
<span className="text-[10px] text-[color:var(--muted)]">{ar.role}</span>
<span className="text-[13px] font-semibold text-foreground">{ar.agentName}</span>
<span className="text-[10px] text-muted-foreground">{ar.role}</span>
</div>
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-[color:var(--muted)]">
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</span> : null}
{ar.tasksBlocked > 0 ? <span className="text-[color:var(--danger)]">{ar.tasksBlocked} blocked</span> : null}
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-muted-foreground">
{ar.tasksInProgress > 0 ? <span className="text-primary">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-green-600 dark:text-green-400">{ar.tasksCompleted24h} done</span> : null}
{ar.tasksBlocked > 0 ? <span className="text-destructive">{ar.tasksBlocked} blocked</span> : null}
{ar.costLast24h > 0 ? <span className="tabular-nums">${ar.costLast24h.toFixed(2)}</span> : null}
</div>
{ar.highlights.length > 0 ? (
<div className="mt-1 text-[10px] text-[color:var(--muted)]">{ar.highlights.join(" \u00B7 ")}</div>
<div className="mt-1 text-[10px] text-muted-foreground">{ar.highlights.join(" \u00B7 ")}</div>
) : null}
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button>
))}
</div>
+22 -22
View File
@@ -10,19 +10,19 @@ import {
import type { InboxItemType } from "@shared/types";
export const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = {
pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" },
failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" },
budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" },
blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" },
review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" },
agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" },
pending_approval: { icon: ShieldCheck, color: "var(--destructive)", bg: "var(--muted-foreground)", label: "Approval" },
failed_run: { icon: XCircle, color: "var(--destructive)", bg: "var(--destructive)", label: "Failed Run" },
budget_warning: { icon: CurrencyCircleDollar, color: "var(--destructive)", bg: "var(--muted-foreground)", label: "Budget" },
blocked_task: { icon: Warning, color: "var(--destructive)", bg: "var(--destructive)", label: "Blocked" },
review_needed: { icon: Eye, color: "var(--primary)", bg: "var(--primary)", label: "Review" },
agent_message: { icon: ChatCircle, color: "var(--primary)", bg: "var(--primary)", label: "Message" },
};
export const severityColors: 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(--line)]",
critical: "border-l-[color:var(--destructive)]",
high: "border-l-[color:var(--destructive)]",
medium: "border-l-[color:var(--primary)]",
low: "border-l-[color:var(--border)]",
};
export function CompactMetric({ label, value, tone = "default", subtitle }: {
@@ -32,37 +32,37 @@ export function CompactMetric({ label, value, tone = "default", subtitle }: {
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",
};
return (
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className="rounded-lg border border-border bg-card px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{label}</div>
<div className={`text-[22px] font-bold ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="text-[10px] text-[color:var(--muted)]">{subtitle}</div> : null}
{subtitle ? <div className="text-[10px] text-muted-foreground">{subtitle}</div> : null}
</div>
);
}
export function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) {
if (total === 0) return <div className="h-3 rounded-full bg-[color:var(--panel-soft)]" />;
if (total === 0) return <div className="h-3 rounded-full bg-muted" />;
const successPct = (succeeded / total) * 100;
const failPct = (failed / total) * 100;
return (
<div className="flex h-3 overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<div className="flex h-3 overflow-hidden rounded-full bg-muted">
<motion.div
className="h-full bg-[color:var(--success)]"
className="h-full bg-green-600 dark:bg-green-400"
initial={{ width: 0 }}
animate={{ width: `${successPct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
<motion.div
className="h-full bg-[color:var(--danger)]"
className="h-full bg-destructive"
initial={{ width: 0 }}
animate={{ width: `${failPct}%` }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
@@ -77,7 +77,7 @@ export function AgentCreator({
options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.label }))}
/>
{executionReadyConnectors.length === 0 ? (
<p className="text-[12px] text-[color:var(--warn)]">
<p className="text-[12px] text-yellow-600 dark:text-yellow-400">
No execution-ready connector is available. Fix connector health before requesting hires.
</p>
) : null}
@@ -127,7 +127,7 @@ export function AgentCreator({
onChange={setCapabilities}
placeholder="What this agent does"
/>
<p className="text-[12px] text-[color:var(--muted)]">
<p className="text-[12px] text-muted-foreground">
New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.
</p>
<ActionButton
@@ -173,7 +173,7 @@ export function AgentCreator({
icon={Robot}
disabled={!selectedConnector}
/>
{requestError ? <p className="text-[12px] text-[color:var(--danger)]">{requestError}</p> : null}
{requestError ? <p className="text-[12px] text-destructive">{requestError}</p> : null}
</div>
);
}
@@ -128,7 +128,7 @@ export function AgentEditor({
]}
/>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
<p className="text-[12px] text-muted-foreground">
Pending hires must be approved or rejected from Hiring or Approvals. Status changes are intentionally locked here.
</p>
) : null}
@@ -140,7 +140,7 @@ export function AgentEditor({
options={connectorOptions}
/>
{selectedConnectorIssue ? (
<p className="text-[12px] text-[color:var(--warn)]">
<p className="text-[12px] text-yellow-600 dark:text-yellow-400">
{selectedConnectorIssue}
</p>
) : null}
@@ -193,8 +193,8 @@ export function AgentEditor({
<HeartbeatSection agent={agent} companyId={companyId} pendingApproval={pendingApproval} persistedConnectorIssue={persistedConnectorIssue} onSaved={onSaved} />
{/* Skills section (D-04 dual access) */}
<div className="mt-6 border-t border-[color:var(--line)] pt-4">
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)] mb-3">Skills</h3>
<div className="mt-6 border-t border-border pt-4">
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">Skills</h3>
<SkillAgentToggles mode="agent" companyId={companyId} agentId={agent.id} skills={skills} />
</div>
@@ -271,11 +271,11 @@ export function AgentEditor({
)}
</div>
{pendingApproval ? (
<p className="text-[12px] text-[color:var(--muted)]">
<p className="text-[12px] text-muted-foreground">
Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
</p>
) : null}
{saveError ? <p className="text-[12px] text-[color:var(--danger)]">{saveError}</p> : null}
{saveError ? <p className="text-[12px] text-destructive">{saveError}</p> : null}
</div>
);
}
@@ -38,7 +38,7 @@ export function AgentRoster({
return (
<div
ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]"
className="h-full overflow-auto border-y border-border"
role="listbox"
aria-label="Agent roster"
onKeyDown={handleKeyDown}
@@ -73,29 +73,29 @@ export function AgentRoster({
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
className={`focus-ring outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--accent)] ring-inset" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 outline-none ${virtualRow.index === focusedIndex ? "ring-1 ring-[color:var(--primary)] ring-inset" : ""}`}
>
<button
type="button"
onClick={() => onSelect(agent.id)}
tabIndex={-1}
className={`block w-full border-b border-[color:var(--line)] px-4 py-3 text-left last:border-b-0 cursor-pointer transition-colors duration-150 ${isSelected ? "bg-[color:var(--accent-soft)] border-l-[3px] border-l-[color:var(--accent)]" : "hover:bg-[color:var(--panel-soft)]"}`}
className={`block w-full border-b border-border px-4 py-3 text-left last:border-b-0 cursor-pointer transition-colors duration-150 ${isSelected ? "bg-accent border-l-[3px] border-l-[color:var(--primary)]" : "hover:bg-muted"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
<CheckCircle size={16} weight="fill" className="text-primary" />
)}
<div>
<div className={`truncate text-[14px] font-semibold text-[color:var(--text)]`}>{agent.name}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">
<div className={`truncate text-[14px] font-semibold text-foreground`}>{agent.name}</div>
<div className="truncate text-[11px] text-muted-foreground">
{agent.role} · {agent.title || "No title"}
</div>
</div>
</div>
<StatusPill status={agent.status} />
</div>
<div className="flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
<span>Connector: {connector?.label ?? agent.connectorId}</span>
{workspace && <span>Workspace: {workspace.name}</span>}
</div>
@@ -30,19 +30,19 @@ export function GoalList({
<ListRow
key={goal.id}
onClick={() => onSelect(goal.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-accent" : "hover:bg-muted"}`}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--accent)]" />
<CheckCircle size={16} weight="fill" className="shrink-0 text-primary" />
)}
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{goal.title}</div>
<div className="truncate text-[14px] font-semibold text-foreground">{goal.title}</div>
</div>
<StatusPill status={goal.status} />
</div>
<div className="line-clamp-2 text-[13px] text-[color:var(--muted)]">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[11px] text-[color:var(--muted)]">
<div className="line-clamp-2 text-[13px] text-muted-foreground">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[11px] text-muted-foreground">
{parentGoal && <span>Parent: {parentGoal.title}</span>}
{ownerAgent && <span>Owner: {ownerAgent.name}</span>}
</div>
@@ -73,10 +73,10 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
const zoomOut = () => { const nz = Math.max(zoom * 0.8, 0.2); const c = containerRef.current; if (c) { const cx = c.clientWidth / 2; const cy = c.clientHeight / 2; const s = nz / zoom; setPan({ x: cx - s * (cx - pan.x), y: cy - s * (cy - pan.y) }); } setZoom(nz); };
return (
<div ref={containerRef} className="relative min-h-[400px] h-[clamp(400px,60vh,720px)] w-full overflow-hidden rounded-[8px] border border-[color:var(--line)] bg-[color:var(--bg)]" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
<div ref={containerRef} className="relative min-h-[400px] h-[clamp(400px,60vh,720px)] w-full overflow-hidden rounded-lg border border-border bg-background" style={{ cursor: dragging ? "grabbing" : "grab" }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onWheel={handleWheel}>
<div className="absolute right-3 top-3 z-10 flex flex-col gap-1">
{[{ label: "+", ariaLabel: "Zoom in", action: zoomIn }, { label: "\u2212", ariaLabel: "Zoom out", action: zoomOut }, { label: "Fit", ariaLabel: "Fit chart to screen", action: fitToScreen }].map((btn) => (
<button key={btn.ariaLabel} className="flex h-7 w-7 items-center justify-center rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] text-[12px] text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]" onClick={btn.action} aria-label={btn.ariaLabel}>{btn.label}</button>
<button key={btn.ariaLabel} className="flex h-7 w-7 items-center justify-center rounded-lg border border-border bg-card text-[12px] text-foreground transition hover:bg-muted" onClick={btn.action} aria-label={btn.ariaLabel}>{btn.label}</button>
))}
</div>
<svg className="pointer-events-none absolute inset-0" style={{ width: "100%", height: "100%" }}>
@@ -85,7 +85,7 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
const x1 = parent.x + CARD_W / 2, y1 = parent.y + CARD_H, x2 = child.x + CARD_W / 2, y2 = child.y;
const midY = (y1 + y2) / 2;
const isChildRunning = child.agent.status === "running";
return <path key={`${parent.agent.id}-${child.agent.id}`} d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`} fill="none" stroke={isChildRunning ? "var(--success)" : "var(--line-strong)"} strokeWidth={isChildRunning ? 2 : 1.5} strokeDasharray={isChildRunning ? "6 3" : "none"} opacity={isChildRunning ? 0.8 : 0.5} />;
return <path key={`${parent.agent.id}-${child.agent.id}`} d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`} fill="none" stroke={isChildRunning ? "var(--primary)" : "var(--border)"} strokeWidth={isChildRunning ? 2 : 1.5} strokeDasharray={isChildRunning ? "6 3" : "none"} opacity={isChildRunning ? 0.8 : 0.5} />;
})}
</g>
</svg>
@@ -100,27 +100,27 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
return (
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-[8px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 ${isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--danger)] border-opacity-50" : isPending ? "border-[color:var(--warn)] border-dashed" : "border-[color:var(--line)] hover:border-[color:var(--line-strong)]"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
<div className={`rounded-t-[7px] ${isCEO ? "h-[4px] bg-[color:var(--accent)]" : node.children.length > 0 ? "h-[3px] bg-[color:var(--warn)]" : "h-[2px] bg-[color:var(--muted)] opacity-30"}`} />
<div key={node.agent.id} data-org-card className={`absolute cursor-pointer select-none rounded-lg border bg-card shadow-sm transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 ${isRunning ? "border-[color:var(--primary)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" : isError ? "border-[color:var(--destructive)] border-opacity-50" : isPending ? "border-[color:var(--destructive)] border-dashed" : "border-border hover:border-border"}`} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} onClick={() => onSelectAgent?.(node.agent.id)}>
<div className={`rounded-t-[7px] ${isCEO ? "h-[4px] bg-primary" : node.children.length > 0 ? "h-[3px] bg-yellow-600 dark:bg-yellow-400" : "h-[2px] bg-[color:var(--muted-foreground)] opacity-30"}`} />
<div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0">
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>{initials}</div>
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[13px] font-bold ${isActive ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400" : isError ? "bg-red-100 dark:bg-red-900/50 text-destructive" : "bg-muted text-muted-foreground"}`}>{initials}</div>
{isRunning ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5" aria-hidden="true">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor, willChange: "transform" }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--card)]" style={{ backgroundColor: dotColor }} />
</span>
) : (
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} aria-hidden="true" />
<span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--card)]" style={{ backgroundColor: dotColor }} aria-hidden="true" />
)}
<span className="sr-only">{`Status: ${node.agent.status}`}</span>
</div>
<div className="flex min-w-0 flex-1 flex-col items-start">
<span className="truncate text-[14px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">{node.agent.title || node.agent.role}</span>
<span className="truncate text-[14px] font-semibold leading-tight text-foreground">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-muted-foreground">{node.agent.title || node.agent.role}</span>
<div className="mt-1.5 flex items-center gap-2">
<span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{connectorLabel}</span>
{node.agent.heartbeatEnabled ? <Heartbeat size={10} className="text-[color:var(--success)]" /> : null}
<span className="rounded bg-muted px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{connectorLabel}</span>
{node.agent.heartbeatEnabled ? <Heartbeat size={10} className="text-green-600 dark:text-green-400" /> : null}
</div>
</div>
</div>
@@ -31,19 +31,19 @@ export function ProjectList({
<ListRow
key={project.id}
onClick={() => onSelect(project.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-accent" : "hover:bg-muted"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
<CheckCircle size={16} weight="fill" className="text-primary" />
)}
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{project.name}</div>
<div className="truncate text-[14px] font-semibold text-foreground">{project.name}</div>
</div>
<StatusPill status={project.status} />
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">{project.description}</div>
<div className="flex flex-wrap gap-4 text-[11px] text-[color:var(--muted)]">
<div className="mb-2 text-[13px] text-muted-foreground">{project.description}</div>
<div className="flex flex-wrap gap-4 text-[11px] text-muted-foreground">
<span>
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
</span>
@@ -8,13 +8,13 @@ export function BudgetBar({ spent, budget }: { spent: number; budget: number })
const overBudget = spent > budget && budget > 0;
return (
<div className="flex items-center gap-2">
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-[color:var(--line)]">
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-[color:var(--border)]">
<div
className={`h-full rounded-full transition-all ${overBudget ? "bg-[color:var(--danger)]" : "bg-[color:var(--success)]"}`}
className={`h-full rounded-full transition-all ${overBudget ? "bg-destructive" : "bg-green-600 dark:bg-green-400"}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className={`text-[11px] ${overBudget ? "text-[color:var(--danger)]" : "text-[color:var(--muted)]"}`}>
<span className={`text-[11px] ${overBudget ? "text-destructive" : "text-muted-foreground"}`}>
{formatMoney(spent)} / {formatMoney(budget)}
</span>
</div>
@@ -31,8 +31,8 @@ export function ConfirmDelete({
onCancel: () => void;
}) {
return (
<div className="flex items-center gap-3 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3">
<span className="text-[13px] text-[color:var(--danger)]">Delete {entityLabel}?</span>
<div className="flex items-center gap-3 rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3">
<span className="text-[13px] text-destructive">Delete {entityLabel}?</span>
<ActionButton label="Yes, delete" tone="danger" onClick={onConfirm} icon={Trash} />
<ActionButton label="Cancel" onClick={onCancel} icon={XCircle} />
</div>
@@ -63,10 +63,10 @@ export function HeartbeatSection({
onSaved: () => Promise<void>;
}) {
return (
<div className="rounded-lg border border-[color:var(--line)] p-3">
<div className="rounded-lg border border-border p-3">
<div className="mb-2 flex items-center gap-2">
<Heartbeat size={16} className="text-[color:var(--accent)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Continuous Execution</span>
<Heartbeat size={16} className="text-primary" />
<span className="text-[13px] font-semibold text-foreground">Continuous Execution</span>
</div>
<div className="mb-2 flex items-center gap-3">
<label className="flex cursor-pointer items-center gap-2">
@@ -80,12 +80,12 @@ export function HeartbeatSection({
await onSaved();
})()
}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<span className="text-[13px] text-[color:var(--text)]">Enable heartbeat</span>
<span className="text-[13px] text-foreground">Enable heartbeat</span>
</label>
{agent.heartbeatEnabled && (
<span className="text-[11px] text-[color:var(--muted)]">
<span className="text-[11px] text-muted-foreground">
Every {agent.heartbeatIntervalSec}s{agent.lastHeartbeatAt ? ` · Last: ${formatTime(agent.lastHeartbeatAt)}` : ""}
</span>
)}
@@ -118,7 +118,7 @@ export function HeartbeatSection({
}
/>
{persistedConnectorIssue ? (
<p className="mt-2 text-[12px] text-[color:var(--warn)]">
<p className="mt-2 text-[12px] text-yellow-600 dark:text-yellow-400">
{persistedConnectorIssue} Save a ready connector before waking this agent again.
</p>
) : null}
@@ -86,13 +86,13 @@ export function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; c
}
export const statusDotColor: Record<string, string> = {
running: "var(--accent)",
active: "var(--success)",
paused: "var(--warn)",
idle: "var(--muted)",
error: "var(--danger)",
terminated: "var(--muted)",
pending_approval: "var(--warn)",
running: "var(--primary)",
active: "var(--primary)",
paused: "var(--destructive)",
idle: "var(--muted-foreground)",
error: "var(--destructive)",
terminated: "var(--muted-foreground)",
pending_approval: "var(--destructive)",
};
export const adapterLabels: Record<string, string> = {
+8 -8
View File
@@ -29,8 +29,8 @@ interface RunDetailProps {
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between py-2">
<span className="text-[12px] text-[color:var(--muted)]">{label}</span>
<span className="text-[13px] text-[color:var(--text)]">{children}</span>
<span className="text-[12px] text-muted-foreground">{label}</span>
<span className="text-[13px] text-foreground">{children}</span>
</div>
);
}
@@ -72,7 +72,7 @@ export function RunDetail({
</div>
)}
>
<div className="divide-y divide-[color:var(--line)]">
<div className="divide-y divide-[color:var(--border)]">
<DetailRow label="Status"><StatusPill status={selectedRun.status} /></DetailRow>
<DetailRow label="Agent">{agent?.name ?? selectedRun.agentId}</DetailRow>
<DetailRow label="Connector">{selectedRun.connectorId}</DetailRow>
@@ -82,7 +82,7 @@ export function RunDetail({
<DetailRow label="Cost">{formatMoney(selectedRun.costUsd)}</DetailRow>
</div>
{selectedRun.errorMessage ? (
<div className="mt-4 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="mt-4 rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive">
{selectedRun.errorMessage}
</div>
) : null}
@@ -118,15 +118,15 @@ export function RunDetail({
)}
>
{logState.error ? (
<div className="rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3 text-[13px] text-[color:var(--danger)]">
<div className="rounded-lg border border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 px-4 py-3 text-[13px] text-destructive">
{logState.error}
</div>
) : null}
<div className="mb-3 flex items-center justify-between text-[11px] text-[color:var(--muted)]">
<div className="mb-3 flex items-center justify-between text-[11px] text-muted-foreground">
<span>{logState.loading ? "Refreshing\u2026" : logState.eof ? "At latest tail" : "Tail window loaded"}</span>
</div>
<div className="max-h-[420px] overflow-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3">
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
<div className="max-h-[420px] overflow-auto rounded-lg border border-border bg-card px-4 py-3">
<pre className="font-mono whitespace-pre-wrap text-[12px] leading-relaxed text-muted-foreground">
{logState.content || "No log output captured yet."}
</pre>
</div>
+4 -4
View File
@@ -71,19 +71,19 @@ export function RunQueue({
<ListRow
key={run.id}
onClick={() => onSelectRun(run.id)}
className={selectedRun?.id === run.id ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}
className={selectedRun?.id === run.id ? "bg-accent" : "hover:bg-muted"}
>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-[color:var(--text)]">{title}</div>
<div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">
<div className="truncate text-[13px] font-semibold text-foreground">{title}</div>
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
{agent?.name ?? "Unknown agent"} via {run.connectorId} {workspace ? `\u00B7 ${workspace.name}` : ""}
</div>
</div>
<StatusPill status={run.status} />
</div>
<div className="flex items-center justify-between gap-3 text-[11px] text-[color:var(--muted)]">
<div className="flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
<span className="truncate">{supportingText}</span>
<span className="shrink-0">{timeAgo(run.updatedAt)}</span>
</div>
+8 -8
View File
@@ -45,16 +45,16 @@ export function RunSummaryCard({
tone?: "default" | "danger";
}) {
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-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
<div className={`mt-1 text-[28px] font-semibold tracking-[-0.03em] ${tone === "danger" ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}>
<div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground">{label}</div>
<div className={`mt-1 text-[28px] font-semibold tracking-[-0.03em] ${tone === "danger" ? "text-destructive" : "text-foreground"}`}>
{value}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{detail}</div>
<div className="mt-1 text-[12px] text-muted-foreground">{detail}</div>
</div>
<div className={`flex h-10 w-10 items-center justify-center rounded-[8px] ${tone === "danger" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"}`}>
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${tone === "danger" ? "bg-red-100 dark:bg-red-900/50 text-destructive" : "bg-muted text-muted-foreground"}`}>
<Icon size={18} />
</div>
</div>
@@ -64,9 +64,9 @@ export function RunSummaryCard({
export function RunMeta({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div>
<div className="mt-1 text-[13px] text-[color:var(--text)]">{value}</div>
<div className="rounded-lg border border-border bg-muted px-3.5 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{label}</div>
<div className="mt-1 text-[13px] text-foreground">{value}</div>
</div>
);
}
@@ -62,7 +62,7 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
if (items.length === 0) {
return (
<p className="px-3 py-4 text-center text-[13px] text-[color:var(--muted)]">
<p className="px-3 py-4 text-center text-[13px] text-muted-foreground">
{mode === "skill" ? "No agents in this company" : "No skills imported yet"}
</p>
);
@@ -78,15 +78,15 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
key={item.id}
type="button"
disabled={isToggling}
className="flex w-full items-center gap-3 border-b border-[color:var(--line)] px-3 py-2 text-left transition hover:bg-[color:var(--panel-soft)] disabled:opacity-60"
className="flex w-full items-center gap-3 border-b border-border px-3 py-2 text-left transition hover:bg-muted disabled:opacity-60"
onClick={() => void handleToggle(item.id)}
>
{/* Toggle indicator */}
<span
className={`flex h-[18px] w-[32px] shrink-0 items-center rounded-full p-[2px] transition-colors ${
isAssigned
? "bg-[color:var(--accent)]"
: "bg-[color:var(--line)]"
? "bg-primary"
: "bg-[color:var(--border)]"
}`}
>
<span
@@ -96,11 +96,11 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
/>
</span>
<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">
{item.name}
</div>
{item.detail ? (
<div className="truncate text-[12px] text-[color:var(--muted)]">
<div className="truncate text-[12px] text-muted-foreground">
{item.detail}
</div>
) : null}
+30 -30
View File
@@ -134,7 +134,7 @@ export function SkillDetail({
if (loading) {
return (
<div className="flex flex-1 items-center justify-center p-8 text-[color:var(--muted)]">
<div className="flex flex-1 items-center justify-center p-8 text-muted-foreground">
<div className="text-[13px]">Loading skill...</div>
</div>
);
@@ -142,7 +142,7 @@ export function SkillDetail({
if (!detail) {
return (
<div className="flex flex-1 items-center justify-center p-8 text-[color:var(--muted)]">
<div className="flex flex-1 items-center justify-center p-8 text-muted-foreground">
<div className="text-[13px]">Skill not found</div>
</div>
);
@@ -165,15 +165,15 @@ export function SkillDetail({
return (
<div className="min-w-0">
{/* Header section */}
<div className="border-b border-[color:var(--line)] px-5 py-4">
<div className="border-b border-border px-5 py-4">
<div className="flex items-start gap-3">
<SourceIcon size={20} className="mt-1 shrink-0 text-[color:var(--muted)]" />
<SourceIcon size={20} className="mt-1 shrink-0 text-muted-foreground" />
<div className="min-w-0">
<h1 className="text-[20px] font-semibold leading-tight text-[color:var(--text)]">
<h1 className="text-[20px] font-semibold leading-tight text-foreground">
{detail.name}
</h1>
{detail.description ? (
<p className="mt-1 max-w-3xl text-[13px] text-[color:var(--muted)]">
<p className="mt-1 max-w-3xl text-[13px] text-muted-foreground">
{detail.description}
</p>
) : null}
@@ -182,35 +182,35 @@ export function SkillDetail({
</div>
{/* Metadata row */}
<div className="space-y-3 border-b border-[color:var(--line)] px-5 pt-4 pb-4">
<div className="space-y-3 border-b border-border px-5 pt-4 pb-4">
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-[13px]">
{/* Source */}
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Source</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Source</span>
<span className="flex items-center gap-1.5">
<SourceIcon size={14} className="text-[color:var(--muted)]" />
<span className="text-[color:var(--text)]">{sourceLabel[detail.sourceType] ?? detail.sourceType}</span>
<SourceIcon size={14} className="text-muted-foreground" />
<span className="text-foreground">{sourceLabel[detail.sourceType] ?? detail.sourceType}</span>
</span>
</div>
{/* Trust level */}
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Trust</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Trust</span>
<StatusPill status={trustToneMap[detail.trustLevel] ?? "default"} />
<span className="text-[color:var(--text)]">{trustLabelMap[detail.trustLevel] ?? detail.trustLevel}</span>
<span className="text-foreground">{trustLabelMap[detail.trustLevel] ?? detail.trustLevel}</span>
</div>
{/* Compatibility */}
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Compat</span>
<span className="text-[color:var(--text)]">{detail.compatibility || "all"}</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Compat</span>
<span className="text-foreground">{detail.compatibility || "all"}</span>
</div>
{/* Ref (for GitHub sources) */}
{isGitHubSource ? (
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Ref</span>
<span className="font-mono text-[12px] text-[color:var(--text)]">{shortRef(detail.sourceRef)}</span>
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Ref</span>
<span className="font-mono text-[12px] text-foreground">{shortRef(detail.sourceRef)}</span>
</div>
) : null}
</div>
@@ -225,11 +225,11 @@ export function SkillDetail({
onClick={() => void handleCheckUpdate()}
/>
{updateStatus?.hasUpdate ? (
<span className="inline-flex items-center gap-1 rounded-full bg-[color:var(--accent-soft)] px-2.5 py-0.5 text-[12px] font-medium text-[color:var(--accent-text)]">
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-2.5 py-0.5 text-[12px] font-medium text-primary">
Update available: {shortRef(updateStatus.currentRef)} &rarr; {shortRef(updateStatus.latestRef)}
</span>
) : updateStatus && !updateStatus.hasUpdate ? (
<span className="text-[12px] text-[color:var(--muted)]">Up to date</span>
<span className="text-[12px] text-muted-foreground">Up to date</span>
) : null}
</div>
) : null}
@@ -240,7 +240,7 @@ export function SkillDetail({
{/* File tree sidebar + content */}
<div className="grid grid-cols-[12rem_minmax(0,1fr)]">
{/* File tree sidebar */}
<div className="border-r border-[color:var(--line)] py-2">
<div className="border-r border-border py-2">
{treeNodes.length > 0 ? (
<SkillFileTree
nodes={treeNodes}
@@ -250,18 +250,18 @@ export function SkillDetail({
onSelectPath={setSelectedSkillFilePath}
/>
) : (
<p className="px-3 py-2 text-[12px] text-[color:var(--muted)]">No files</p>
<p className="px-3 py-2 text-[12px] text-muted-foreground">No files</p>
)}
</div>
{/* Content area */}
<div className="p-4">
{/* File path bar */}
<div className="mb-3 flex items-center gap-1 text-[12px] text-[color:var(--muted)]">
<div className="mb-3 flex items-center gap-1 text-[12px] text-muted-foreground">
{selectedSkillFilePath.split("/").map((segment, i, arr) => (
<span key={i} className="flex items-center gap-1">
{i > 0 ? <span className="opacity-40">/</span> : null}
<span className={i === arr.length - 1 ? "font-medium text-[color:var(--text)]" : ""}>
<span className={i === arr.length - 1 ? "font-medium text-foreground" : ""}>
{segment}
</span>
</span>
@@ -270,35 +270,35 @@ export function SkillDetail({
{/* Content */}
{isSkillMd ? (
<pre className="rounded-lg bg-[color:var(--panel)] p-4 font-mono text-[13px] text-[color:var(--text)] overflow-auto whitespace-pre-wrap">
<pre className="rounded-lg bg-card p-4 font-mono text-[13px] text-foreground overflow-auto whitespace-pre-wrap">
{fileContent}
</pre>
) : selectedFileEntry ? (
<div className="rounded-lg bg-[color:var(--panel)] p-4 text-[13px] text-[color:var(--muted)]">
<p className="font-medium text-[color:var(--text)]">{selectedFileEntry.path}</p>
<div className="rounded-lg bg-card p-4 text-[13px] text-muted-foreground">
<p className="font-medium text-foreground">{selectedFileEntry.path}</p>
<p className="mt-1">Kind: {selectedFileEntry.kind}</p>
<p className="mt-1 text-[12px] italic">File content preview is not available for non-SKILL.md files.</p>
</div>
) : (
<p className="text-[13px] text-[color:var(--muted)]">Select a file to view its details.</p>
<p className="text-[13px] text-muted-foreground">Select a file to view its details.</p>
)}
</div>
</div>
</div>
{/* Agent assignments section */}
<div className="border-t border-[color:var(--line)] px-5 py-4">
<h3 className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)] mb-3">
<div className="border-t border-border px-5 py-4">
<h3 className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">
Agent Assignments
</h3>
<SkillAgentToggles mode="skill" companyId={companyId} skillId={skillId} agents={agents} />
</div>
{/* Delete section */}
<div className="border-t border-[color:var(--line)] px-5 py-4">
<div className="border-t border-border px-5 py-4">
{confirmingDelete ? (
<div className="flex items-center gap-3">
<p className="text-[13px] text-[color:var(--danger)]">
<p className="text-[13px] text-destructive">
Delete &ldquo;{detail.name}&rdquo;? This cannot be undone.
</p>
<ActionButton
@@ -90,7 +90,7 @@ export function SkillFileTree({
<div key={node.path ?? node.name}>
<button
type="button"
className="flex min-h-9 w-full items-center gap-2 text-left text-[13px] text-[color:var(--muted)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="flex min-h-9 w-full items-center gap-2 text-left text-[13px] text-muted-foreground hover:bg-muted hover:text-foreground"
style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onToggleDir(node.path)}
>
@@ -118,10 +118,10 @@ export function SkillFileTree({
<button
key={node.path ?? node.name}
type="button"
className={`flex min-h-9 w-full items-center gap-2 text-left text-[13px] hover:bg-[color:var(--panel-soft)] ${
className={`flex min-h-9 w-full items-center gap-2 text-left text-[13px] hover:bg-muted ${
isSelected
? "text-[color:var(--text)] font-medium"
: "text-[color:var(--muted)]"
? "text-foreground font-medium"
: "text-muted-foreground"
}`}
style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onSelectPath(node.path)}
@@ -114,7 +114,7 @@ export function SkillImportModal({
<Modal open={open} title="Import Skill" onClose={handleClose} width="560px">
<div className="space-y-5">
{/* Tab bar */}
<div className="flex gap-1 border-b border-[color:var(--line)]">
<div className="flex gap-1 border-b border-border">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
@@ -124,8 +124,8 @@ export function SkillImportModal({
type="button"
className={`flex items-center gap-1.5 px-3 py-2 text-[12px] font-medium transition-colors ${
isActive
? "border-b-2 border-[color:var(--accent)] text-[color:var(--text)]"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
? "border-b-2 border-[color:var(--primary)] text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => {
setActiveTab(tab.id);
@@ -144,7 +144,7 @@ export function SkillImportModal({
{activeTab === "local" ? (
<div className="flex gap-2">
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground"
value={source}
onChange={(e) => setSource(e.target.value)}
placeholder={placeholder.local}
@@ -153,7 +153,7 @@ export function SkillImportModal({
</div>
) : (
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground"
value={source}
onChange={(e) => setSource(e.target.value)}
placeholder={placeholder[activeTab]}
@@ -161,8 +161,8 @@ export function SkillImportModal({
)}
{activeTab === "skills_sh" && (
<p className="text-[12px] text-[color:var(--muted)]">
Enter a skills.sh install command (e.g., <code className="rounded bg-[color:var(--panel-soft)] px-1 py-0.5 font-mono text-[11px]">npx skills.sh install skill-name</code>) or a direct skills.sh URL.
<p className="text-[12px] text-muted-foreground">
Enter a skills.sh install command (e.g., <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">npx skills.sh install skill-name</code>) or a direct skills.sh URL.
</p>
)}
</div>
+5 -5
View File
@@ -62,7 +62,7 @@ export function SkillList({
if (filteredSkills.length === 0) {
return (
<div className="px-4 py-6 text-[13px] text-[color:var(--muted)]">
<div className="px-4 py-6 text-[13px] text-muted-foreground">
No skills match this filter.
</div>
);
@@ -79,10 +79,10 @@ export function SkillList({
const filePath = selectedPaths[skill.id] ?? "SKILL.md";
return (
<div key={skill.id} className="border-b border-[color:var(--line)]">
<div key={skill.id} className="border-b border-border">
<div
className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-[color:var(--panel-soft)] ${
isSelected ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"
className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-muted ${
isSelected ? "text-foreground" : "text-muted-foreground"
}`}
>
<SourceIcon
@@ -101,7 +101,7 @@ export function SkillList({
</button>
<button
type="button"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[4px] text-[color:var(--muted)] opacity-60 transition-opacity hover:bg-[color:var(--panel-soft)] hover:opacity-100"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[4px] text-muted-foreground opacity-60 transition-opacity hover:bg-muted hover:opacity-100"
onClick={() => handleToggleSkill(skill.id)}
aria-label={isExpanded ? "Collapse file tree" : "Expand file tree"}
>
@@ -60,20 +60,20 @@ export function SkillsSection({
return (
<div className="grid h-full grid-cols-1 xl:grid-cols-[19rem_minmax(0,1fr)]">
{/* Left column -- skill list sidebar */}
<aside className="flex flex-col overflow-y-auto border-r border-[color:var(--line)]">
<aside className="flex flex-col overflow-y-auto border-r border-border">
{/* Header */}
<div className="flex shrink-0 items-center gap-3 px-4 py-3">
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-[color:var(--text)]">
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">
Skills
</h2>
{skills.length > 0 && (
<span className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-full bg-[color:var(--panel-soft)] px-1.5 text-[11px] font-semibold text-[color:var(--muted-strong)]">
<span className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-full bg-muted px-1.5 text-[11px] font-semibold text-muted-foreground">
{skills.length}
</span>
)}
<button
type="button"
className="focus-ring ml-auto flex h-7 w-7 items-center justify-center rounded-[6px] text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-muted hover:text-foreground"
onClick={() => setSkillImportModalOpen(true)}
aria-label="Import skill"
>
@@ -86,11 +86,11 @@ export function SkillsSection({
<div className="relative">
<MagnifyingGlass
size={14}
className="absolute left-3 top-1/2 -translate-y-1/2 text-[color:var(--muted)]"
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
aria-hidden="true"
/>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2 pl-8 pr-3 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
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 py-2 pl-8 pr-3 text-[13px] text-foreground placeholder:text-muted-foreground"
value={skillFilter}
onChange={(e) => setSkillFilter(e.target.value)}
placeholder="Filter skills..."
@@ -108,7 +108,7 @@ export function SkillsSection({
action={
<button
type="button"
className="focus-ring inline-flex items-center gap-2 rounded-[8px] border border-transparent bg-[color:var(--accent)] px-4 py-2.5 text-[12px] font-bold text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(74,93,122,0.35)] transition hover:bg-[color:var(--accent-hover)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex items-center gap-2 rounded-lg border border-transparent bg-primary px-4 py-2.5 text-[12px] font-bold text-primary-foreground shadow-[0_1px_3px_rgba(74,93,122,0.35)] transition hover:bg-primary/90"
onClick={() => setSkillImportModalOpen(true)}
>
<Plus size={14} weight="bold" />
+15 -15
View File
@@ -17,7 +17,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
const cardContent = (
<>
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-bold text-[color:var(--text)]">{account.displayName || account.accountName}</span>
<span className="truncate text-[13px] font-bold text-foreground">{account.displayName || account.accountName}</span>
<span
className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: platform.bgColor, color: platform.color }}
@@ -25,10 +25,10 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
{platform.label}
</span>
</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-muted-foreground">
<span className="truncate">@{account.accountName}</span>
{account.requireApproval ? (
<span className="flex items-center gap-1 text-[color:var(--warn)]">
<span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
<ShieldCheck size={10} />
Approval required
</span>
@@ -39,9 +39,9 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
);
return (
<div className="group flex items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md hover:-translate-y-px">
<div className="group flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3.5 transition duration-150 hover:border-border hover:shadow-md hover:-translate-y-px">
<div
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[8px]"
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: platform.bgColor, color: platform.color }}
>
<PlatformIcon size={18} weight="fill" />
@@ -58,18 +58,18 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<div className="flex items-center gap-2">
<span className={`flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-semibold ${
isLoggedIn
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: account.status === "login_required"
? "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
? "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
: account.status === "suspended"
? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
? "bg-red-100 dark:bg-red-900/50 text-destructive"
: "bg-muted text-muted-foreground"
}`}>
<span className={`h-1.5 w-1.5 rounded-full ${
isLoggedIn ? "bg-[color:var(--success)]"
: account.status === "login_required" ? "bg-[color:var(--warn)]"
: account.status === "suspended" ? "bg-[color:var(--danger)]"
: "bg-[color:var(--muted)]"
isLoggedIn ? "bg-green-600 dark:bg-green-400"
: account.status === "login_required" ? "bg-yellow-600 dark:bg-yellow-400"
: account.status === "suspended" ? "bg-destructive"
: "bg-[color:var(--muted-foreground)]"
}`} />
{account.status === "logged_in" ? "Logged In" : account.status === "active" ? "Active" : account.status.replace("_", " ")}
</span>
@@ -78,7 +78,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<button
type="button"
onClick={onLogin}
className="flex items-center gap-1 rounded-[6px] bg-[color:var(--accent)] px-2.5 py-1.5 text-[10px] font-semibold text-[color:var(--text-on-accent)] transition hover:opacity-90"
className="flex items-center gap-1 rounded-lg bg-primary px-2.5 py-1.5 text-[10px] font-semibold text-primary-foreground transition hover:opacity-90"
>
<SignIn size={12} />
Login
@@ -89,7 +89,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
type="button"
onClick={onDelete}
aria-label="Delete account"
className="rounded-[6px] p-1.5 text-[color:var(--muted)] opacity-0 transition hover:bg-[color:var(--danger-soft)] hover:text-[color:var(--danger)] group-hover:opacity-100 group-focus-within:opacity-100"
className="rounded-lg p-1.5 text-muted-foreground opacity-0 transition hover:bg-red-100 dark:bg-red-900/50 hover:text-destructive group-hover:opacity-100 group-focus-within:opacity-100"
>
<Trash size={14} />
</button>
@@ -18,7 +18,7 @@ interface ActionHistoryProps {
export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onNavigate }: ActionHistoryProps) {
if (actions.length === 0) {
return (
<div className="py-8 text-center text-[13px] text-[color:var(--muted)]">
<div className="py-8 text-center text-[13px] text-muted-foreground">
No browser actions yet
</div>
);
@@ -37,36 +37,36 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
return (
<div
key={action.id}
className="flex items-center gap-3 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5 text-[12px]"
className="flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-2.5 text-[12px]"
>
<Icon size={16} className={
isSuccess ? "text-[color:var(--success)]"
: isFailed ? "text-[color:var(--danger)]"
: isRunning ? "text-[color:var(--accent)]"
: "text-[color:var(--muted)]"
isSuccess ? "text-green-600 dark:text-green-400"
: isFailed ? "text-destructive"
: isRunning ? "text-primary"
: "text-muted-foreground"
} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-[color:var(--text)]">{config.label}</span>
<span className="font-medium text-foreground">{config.label}</span>
{agent ? (
<span className="flex items-center gap-1 text-[color:var(--muted)]">
<span className="flex items-center gap-1 text-muted-foreground">
<Robot size={10} />
{agent.name}
</span>
) : null}
</div>
{action.resultSummary ? (
<div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{action.resultSummary}</div>
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">{action.resultSummary}</div>
) : null}
{action.errorMessage ? (
<div className="mt-0.5 truncate text-[11px] text-[color:var(--danger)]">{action.errorMessage}</div>
<div className="mt-0.5 truncate text-[11px] text-destructive">{action.errorMessage}</div>
) : null}
<div className="mt-1 flex flex-wrap items-center gap-2">
{action.taskId ? (
<button
type="button"
onClick={() => onNavigate("tasks", action.taskId!)}
className="rounded-[5px] bg-[color:var(--panel-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
className="rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
>
Open task
</button>
@@ -75,7 +75,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button
type="button"
onClick={() => onNavigate("approvals", action.approvalId!)}
className="rounded-[5px] bg-[color:var(--panel-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
className="rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
>
Open approval
</button>
@@ -84,7 +84,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button
type="button"
onClick={() => void onOpenPath(action.screenshotPath!)}
className="inline-flex items-center gap-1 rounded-[5px] bg-[color:var(--panel-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--muted-strong)] transition hover:text-[color:var(--text)]"
className="inline-flex items-center gap-1 rounded-[5px] bg-muted px-2 py-1 text-[10px] font-medium text-muted-foreground transition hover:text-foreground"
>
<ArrowSquareOut size={10} />
Screenshot
@@ -94,7 +94,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button
type="button"
onClick={() => void onCancelAction(action.id)}
className="inline-flex items-center gap-1 rounded-[5px] bg-[color:var(--danger-soft)] px-2 py-1 text-[10px] font-medium text-[color:var(--danger)] transition hover:brightness-95"
className="inline-flex items-center gap-1 rounded-[5px] bg-red-100 dark:bg-red-900/50 px-2 py-1 text-[10px] font-medium text-destructive transition hover:brightness-95"
>
<ClockCounterClockwise size={10} />
Cancel
@@ -102,15 +102,15 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
) : null}
</div>
</div>
<div className="shrink-0 text-[10px] text-[color:var(--muted)]">
<div className="shrink-0 text-[10px] text-muted-foreground">
{timeAgo(action.createdAt)}
</div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold ${
isSuccess ? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: isFailed ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
: isRunning ? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: action.status === "approval_required" ? "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
isSuccess ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: isFailed ? "bg-red-100 dark:bg-red-900/50 text-destructive"
: isRunning ? "bg-accent text-primary"
: action.status === "approval_required" ? "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
: "bg-muted text-muted-foreground"
}`}>
{action.status.replace("_", " ")}
</span>
@@ -28,29 +28,29 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="w-full max-w-[420px] rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-xl"
className="w-full max-w-[420px] rounded-lg border border-border bg-card p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-5 flex items-center justify-between">
<h3 className="text-[16px] font-bold text-[color:var(--text)]">Add Social Account</h3>
<button type="button" onClick={onClose} className="rounded-[6px] p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel-soft)]">
<h3 className="text-[16px] font-bold text-foreground">Add Social Account</h3>
<button type="button" onClick={onClose} className="rounded-lg p-1 text-muted-foreground hover:bg-muted">
<X size={18} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]">Platform</label>
<label className="mb-1.5 block text-[12px] font-medium text-muted-foreground">Platform</label>
<div className="grid grid-cols-3 gap-1.5">
{(Object.entries(platformConfig) as [SocialPlatform, typeof platformConfig.twitter][]).map(([key, config]) => (
<button
key={key}
type="button"
onClick={() => setPlatform(key)}
className={`flex items-center gap-2 rounded-[8px] border px-3 py-2 text-[11px] font-medium transition ${
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-[11px] font-medium transition ${
platform === key
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "border-[color:var(--line)] text-[color:var(--muted)] hover:border-[color:var(--line-strong)]"
? "border-[color:var(--primary)] bg-accent text-primary"
: "border-border text-muted-foreground hover:border-border"
}`}
>
<span style={{ color: config.color }}>
@@ -76,17 +76,17 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
placeholder="Your display name"
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="rounded-lg border border-border bg-muted px-4 py-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={requireApproval}
onChange={(e) => setRequireApproval(e.target.checked)}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
className="h-4 w-4 rounded border-border accent-[color:var(--primary)]"
/>
<div>
<div className="text-[12px] font-medium text-[color:var(--text)]">Require Board Approval for Posts</div>
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">
<div className="text-[12px] font-medium text-foreground">Require Board Approval for Posts</div>
<div className="mt-0.5 text-[10px] text-muted-foreground">
Agents must get approval before publishing content to this account
</div>
</div>
@@ -98,7 +98,7 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
<button
type="button"
onClick={onClose}
className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
className="rounded-lg px-4 py-2 text-[12px] font-medium text-muted-foreground transition hover:bg-muted"
>
Cancel
</button>
+17 -17
View File
@@ -52,12 +52,12 @@ export function SocialAccountsPanel({
<div className="space-y-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]">
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" />
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Megaphone size={20} weight="fill" className="text-primary" />
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("social.socialMedia")}</h2>
<p className="text-[12px] text-[color:var(--muted)]">
<h2 className="text-[16px] font-bold text-foreground">{t("social.socialMedia")}</h2>
<p className="text-[12px] text-muted-foreground">
{accounts.length} account{accounts.length !== 1 ? "s" : ""} connected
</p>
</div>
@@ -70,8 +70,8 @@ export function SocialAccountsPanel({
/>
</div>
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3">
<div className="flex items-start gap-2 text-[12px] text-[color:var(--accent)]">
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-20 bg-accent px-4 py-3">
<div className="flex items-start gap-2 text-[12px] text-primary">
<Browser size={16} className="mt-0.5 shrink-0" />
<div>
<div className="font-medium">How it works</div>
@@ -84,14 +84,14 @@ export function SocialAccountsPanel({
</div>
</div>
<div className="flex gap-1 rounded-[8px] bg-[color:var(--panel-soft)] p-1">
<div className="flex gap-1 rounded-lg bg-muted p-1">
<button
type="button"
onClick={() => setActiveTab("accounts")}
className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${
className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${
activeTab === "accounts"
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Accounts ({accounts.length})
@@ -99,10 +99,10 @@ export function SocialAccountsPanel({
<button
type="button"
onClick={() => setActiveTab("history")}
className={`flex-1 rounded-[6px] px-3 py-1.5 text-[12px] font-medium transition ${
className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${
activeTab === "history"
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Action History ({actions.length})
@@ -111,10 +111,10 @@ export function SocialAccountsPanel({
{activeTab === "accounts" ? (
accounts.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Megaphone size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<Megaphone size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-muted-foreground">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-muted-foreground">
{t("social.noAccountsDesc")}
</div>
<div className="mt-4">
+57 -57
View File
@@ -2,7 +2,7 @@ import { Component, useEffect, useRef, type ElementType, type ErrorInfo, type Re
export function Badge({ count, tone = "default" }: { count: number; tone?: "default" | "warn" | "danger" }) {
if (count <= 0) return null;
const toneClass = tone === "danger" ? "bg-[color:var(--danger)] text-[color:var(--text-on-accent)]" : tone === "warn" ? "bg-[color:var(--warn)] text-[color:var(--text-on-accent)]" : "bg-[color:var(--muted)] text-[color:var(--text-on-accent)]";
const toneClass = tone === "danger" ? "bg-destructive text-primary-foreground" : tone === "warn" ? "bg-yellow-600 dark:bg-yellow-400 text-primary-foreground" : "bg-[color:var(--muted-foreground)] text-primary-foreground";
return (
<span className={`ml-auto inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full px-1.5 text-[10px] font-bold ${toneClass}`}>
{count > 99 ? "99+" : count}
@@ -33,17 +33,17 @@ export function SectionButton({
aria-current={active ? "page" : undefined}
aria-label={label}
title={label}
className={`focus-ring relative flex w-full items-center justify-center rounded-[6px] p-2 transition duration-150 ${
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative flex w-full items-center justify-center rounded-lg p-2 transition duration-150 ${
active
? "bg-[color:var(--panel-soft)] text-[color:var(--text)]"
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
onClick={onClick}
>
<Icon size={18} weight={active ? "fill" : "regular"} className={active ? "text-[color:var(--accent)]" : ""} />
<Icon size={18} weight={active ? "fill" : "regular"} className={active ? "text-primary" : ""} />
{badge ? (
<span className={`absolute right-1 top-1 h-[6px] w-[6px] rounded-full ${
badgeTone === "danger" ? "bg-[color:var(--danger)]" : badgeTone === "warn" ? "bg-[color:var(--warn)]" : "bg-[color:var(--muted)]"
badgeTone === "danger" ? "bg-destructive" : badgeTone === "warn" ? "bg-yellow-600 dark:bg-yellow-400" : "bg-[color:var(--muted-foreground)]"
}`} />
) : null}
</button>
@@ -52,15 +52,15 @@ export function SectionButton({
return (
<button
aria-current={active ? "page" : undefined}
className={`focus-ring relative flex w-full items-center gap-2.5 rounded-[6px] px-3 py-[7px] text-left transition duration-150 ${
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative flex w-full items-center gap-2.5 rounded-lg px-3 py-[7px] text-left transition duration-150 ${
active
? "bg-[color:var(--accent-soft)] text-[color:var(--text)]"
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`}
onClick={onClick}
>
{active ? <span className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-[color:var(--accent)]" aria-hidden="true" /> : null}
<Icon size={17} weight={active ? "fill" : "regular"} className={active ? "text-[color:var(--accent)]" : ""} />
{active ? <span className="absolute left-0 top-1/2 h-4 w-[3px] -translate-y-1/2 rounded-r-full bg-primary" aria-hidden="true" /> : null}
<Icon size={17} weight={active ? "fill" : "regular"} className={active ? "text-primary" : ""} />
<span className={`text-[13px] tracking-[0.005em] ${active ? "font-bold" : "font-medium"}`}>{label}</span>
{badge ? <Badge count={badge} tone={badgeTone} /> : null}
</button>
@@ -80,14 +80,14 @@ export function Panel({
children: ReactNode;
action?: ReactNode;
className?: string;
tone?: "plain" | "surface";
tone?: "plain" | "bg-card border border-border shadow-sm";
}) {
return (
<section className={`space-y-4 ${className}`}>
<div className={`flex items-end justify-between gap-4 ${tone === "surface" ? "border-b border-[color:var(--line)] pb-3" : ""}`}>
<div className={`flex items-end justify-between gap-4 ${tone === "bg-card border border-border shadow-sm" ? "border-b border-border pb-3" : ""}`}>
<div>
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)]">{eyebrow}</div> : null}
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-[color:var(--text)]">{title}</h2>
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">{eyebrow}</div> : null}
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">{title}</h2>
</div>
{action}
</div>
@@ -122,21 +122,21 @@ export function Modal({
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--text)]/45" onClick={onClose}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--foreground)]/45" onClick={onClose}>
<div
ref={trapRef}
role="dialog"
aria-modal="true"
aria-label={title}
className="flex w-full flex-col rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-xl"
className="flex w-full flex-col rounded-lg border border-border bg-card shadow-xl"
style={{ maxWidth: width, maxHeight: "80vh" }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-5 py-3.5">
<h2 className="text-[16px] font-bold tracking-[-0.015em] text-[color:var(--text)]">{title}</h2>
<div className="flex shrink-0 items-center justify-between border-b border-border px-5 py-3.5">
<h2 className="text-[16px] font-bold tracking-[-0.015em] text-foreground">{title}</h2>
<button
type="button"
className="focus-ring rounded-[6px] p-1 text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg p-1 text-muted-foreground transition hover:text-foreground"
onClick={onClose}
aria-label="Close"
>
@@ -152,7 +152,7 @@ export function Modal({
export function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{label}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{label}</div>
{children}
</label>
);
@@ -184,7 +184,7 @@ export function Input({
id={id}
aria-label={ariaLabel}
type={type}
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] disabled:cursor-not-allowed disabled:opacity-60"
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-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60"
value={value}
placeholder={placeholder}
readOnly={readOnly}
@@ -195,7 +195,7 @@ export function Input({
if (label) {
return (
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{label}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{label}</div>
{input}
</label>
);
@@ -222,7 +222,7 @@ export function TextArea({
}) {
const textarea = (
<textarea
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] disabled:cursor-not-allowed disabled:opacity-60"
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-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60"
value={value}
rows={rows}
placeholder={placeholder}
@@ -234,7 +234,7 @@ export function TextArea({
if (label) {
return (
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{label}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{label}</div>
{textarea}
</label>
);
@@ -259,17 +259,17 @@ export function Select({
}) {
const select = (
<select
className={`focus-ring w-full border bg-[color:var(--panel)] text-[color:var(--text)] ${
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full border bg-card text-foreground ${
compact
? "rounded-[6px] border-[color:var(--line)] px-2.5 py-1.5 text-[13px] font-medium"
: "rounded-[8px] border-[color:var(--line)] px-3.5 py-2.5 text-[14px]"
? "rounded-lg border-border px-2.5 py-1.5 text-[13px] font-medium"
: "rounded-lg border-border px-3.5 py-2.5 text-[14px]"
} disabled:cursor-not-allowed disabled:opacity-60`}
value={value}
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
>
{options.map((option) => (
<option key={option.value} value={option.value} className="bg-[color:var(--panel)] text-[color:var(--text)]">
<option key={option.value} value={option.value} className="bg-card text-foreground">
{option.label}
</option>
))}
@@ -278,7 +278,7 @@ export function Select({
if (label) {
return (
<label className="block">
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{label}</div>
<div className="mb-1.5 text-[12px] font-medium text-muted-foreground">{label}</div>
{select}
</label>
);
@@ -304,14 +304,14 @@ export function ActionButton({
const isDisabled = loading || disabled;
const toneClass =
tone === "accent"
? "border-transparent bg-[color:var(--accent)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(74,93,122,0.35)] hover:bg-[color:var(--accent-hover)]"
? "border-transparent bg-primary text-primary-foreground shadow-[0_1px_3px_rgba(74,93,122,0.35)] hover:bg-primary/90"
: tone === "danger"
? "border-transparent bg-[color:var(--danger)] text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(220,53,69,0.3)] hover:brightness-90"
: "border-[color:var(--line)] bg-[color:var(--panel)] text-[color:var(--text)] hover:bg-[color:var(--panel-soft)]";
? "border-transparent bg-destructive text-primary-foreground shadow-[0_1px_3px_rgba(220,53,69,0.3)] hover:brightness-90"
: "border-border bg-card text-foreground hover:bg-muted";
return (
<button
className={`focus-ring inline-flex items-center gap-2 rounded-[8px] border px-4 py-2.5 text-[12px] font-bold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex items-center gap-2 rounded-lg border px-4 py-2.5 text-[12px] font-bold transition duration-150 active:translate-y-px ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
onClick={onClick}
disabled={isDisabled}
aria-busy={loading}
@@ -335,26 +335,26 @@ export function StatusPill({ status }: { status: string }) {
const warning = ["auth_required", "degraded", "pending", "in_review", "todo", "queued", "not_installed"].includes(status);
const checking = status === "detected";
const toneClass = positive
? "text-[color:var(--success)]"
? "text-green-600 dark:text-green-400"
: negative
? "text-[color:var(--danger)]"
? "text-destructive"
: warning
? "text-[color:var(--warn)]"
: "text-[color:var(--muted-strong)]";
? "text-yellow-600 dark:text-yellow-400"
: "text-muted-foreground";
const dotClass = positive
? "bg-[color:var(--success)]"
? "bg-green-600 dark:bg-green-400"
: negative
? "bg-[color:var(--danger)]"
? "bg-destructive"
: warning
? "bg-[color:var(--warn)]"
: "bg-[color:var(--muted)]";
? "bg-yellow-600 dark:bg-yellow-400"
: "bg-[color:var(--muted-foreground)]";
const displayLabel = checking ? "checking" : status.replaceAll("_", " ");
return (
<span className={`inline-flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] ${toneClass}`}>
{checking ? (
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--muted)]" aria-hidden="true" />
<span className="h-2 w-2 animate-pulse rounded-full bg-[color:var(--muted-foreground)]" aria-hidden="true" />
) : (
<span className={`h-2 w-2 rounded-full ${dotClass}`} aria-hidden="true" />
)}
@@ -373,12 +373,12 @@ export function InlineNotice({
action?: ReactNode;
}) {
const toneClass = tone === "success"
? "border-[color:var(--success)] bg-[color:var(--success-soft)] text-[color:var(--success)]"
? "border-[color:var(--primary)] bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: tone === "warn"
? "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
: "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]";
? "border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
: "border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 text-destructive";
return (
<div className={`rounded-[8px] border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
<div className={`rounded-lg border px-4 py-3 text-[13px] ${toneClass}`} role={tone === "danger" ? "alert" : "status"}>
{action ? (
<div className="flex items-start justify-between gap-3">
<span>{message}</span>
@@ -410,17 +410,17 @@ export function ConfirmDialog({ open, title, message, errorMessage, onConfirm, o
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--text)]/45" onClick={onCancel}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[color:var(--foreground)]/45" onClick={onCancel}>
<div
ref={trapRef}
role="dialog"
aria-modal="true"
aria-labelledby="confirm-dialog-title"
className="w-full max-w-[400px] rounded-[8px] bg-[color:var(--panel)] border border-[color:var(--line)] p-6 shadow-lg"
className="w-full max-w-[400px] rounded-lg bg-card border border-border p-6 shadow-lg"
onClick={e => e.stopPropagation()}
>
<div id="confirm-dialog-title" className="text-[16px] font-semibold text-[color:var(--text)] mb-2">{title}</div>
<div className="text-[13px] text-[color:var(--muted)] mb-6">{message}</div>
<div id="confirm-dialog-title" className="text-[16px] font-semibold text-foreground mb-2">{title}</div>
<div className="text-[13px] text-muted-foreground mb-6">{message}</div>
{errorMessage ? <div className="mb-4"><InlineNotice message={errorMessage} /></div> : null}
<div className="flex gap-3 justify-end">
<ActionButton label="Cancel" onClick={onCancel} />
@@ -442,8 +442,8 @@ export function EmptyState({
}) {
return (
<div className="px-1 py-8 text-center">
<div className="mb-2 text-[15px] font-semibold text-[color:var(--text)]">{title}</div>
{detail ? <div className="mx-auto max-w-[48ch] text-[13px] leading-relaxed text-[color:var(--muted)]">{detail}</div> : null}
<div className="mb-2 text-[15px] font-semibold text-foreground">{title}</div>
{detail ? <div className="mx-auto max-w-[48ch] text-[13px] leading-relaxed text-muted-foreground">{detail}</div> : null}
{action ? <div className="mt-5">{action}</div> : null}
</div>
);
@@ -456,7 +456,7 @@ export function ListFrame({
children: ReactNode;
className?: string;
}) {
return <div className={`border-y border-[color:var(--line)] ${className}`}>{children}</div>;
return <div className={`border-y border-border ${className}`}>{children}</div>;
}
export function ListRow({
@@ -472,14 +472,14 @@ export function ListRow({
return (
<button
type="button"
className={`focus-ring block w-full border-b border-[color:var(--line)] px-5 py-4 text-left last:border-b-0 ${className}`}
className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 block w-full border-b border-border px-5 py-4 text-left last:border-b-0 ${className}`}
onClick={onClick}
>
{children}
</button>
);
}
return <div className={`border-b border-[color:var(--line)] px-5 py-4 last:border-b-0 ${className}`}>{children}</div>;
return <div className={`border-b border-border px-5 py-4 last:border-b-0 ${className}`}>{children}</div>;
}
export function useFocusTrap(active: boolean) {
@@ -550,7 +550,7 @@ export class ErrorBoundary extends Component<{ children: ReactNode; fallback?: R
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="p-8 text-center" role="alert">
<div className="text-[15px] font-semibold text-[color:var(--danger)] mb-2">Something went wrong</div>
<div className="text-[15px] font-semibold text-destructive mb-2">Something went wrong</div>
<ActionButton label="Reload page" onClick={() => window.location.reload()} />
</div>
);
+1 -1
View File
@@ -745,7 +745,7 @@ export const en: Record<string, string> = {
"ops.newMeeting": "New meeting",
"ops.newSprint": "New sprint",
"ops.deleteDocument": "Delete document",
"ops.deleteDocumentMsg": "This removes the document from the company knowledge surface.",
"ops.deleteDocumentMsg": "This removes the document from the company knowledge bg-card border border-border shadow-sm.",
"ops.deleteKnowledgeEntry": "Delete knowledge entry",
"ops.deleteKnowledgeMsg": "This removes the entry from the shared company knowledge base.",
"ops.deleteMeeting": "Delete meeting",