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}> <LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}> <TranslationContext.Provider value={t}>
<div className="flex min-h-screen flex-col items-center justify-center gap-3"> <div className="flex min-h-screen flex-col items-center justify-center gap-3">
<CircleNotch size={24} className="animate-spin text-[color:var(--accent)]" /> <CircleNotch size={24} className="animate-spin text-primary" />
<span className="text-[13px] text-[color:var(--muted)]">{t("app.loading")}</span> <span className="text-[13px] text-muted-foreground">{t("app.loading")}</span>
</div> </div>
</TranslationContext.Provider> </TranslationContext.Provider>
</LocaleContext.Provider> </LocaleContext.Provider>
@@ -726,14 +726,14 @@ export function App() {
return ( return (
<LocaleContext.Provider value={locale}> <LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}> <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)]"}`}> <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-[color:var(--line-strong)] bg-[color:var(--panel)]"> <aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-border bg-card">
{sidebarCollapsed ? ( {sidebarCollapsed ? (
<div className="sidebar-drag flex items-center justify-center px-2 pb-3 pt-[38px]"> <div className="sidebar-drag flex items-center justify-center px-2 pb-3 pt-[38px]">
<button <button
aria-label="Open command palette" aria-label="Open command palette"
title="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)} onClick={() => setCommandPaletteOpen(true)}
> >
<Command size={14} /> <Command size={14} />
@@ -744,32 +744,32 @@ export function App() {
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
<button <button
type="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)} onClick={() => setCompanySwitcherOpen(!companySwitcherOpen)}
> >
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span> <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> </button>
{companySwitcherOpen && ( {companySwitcherOpen && (
<> <>
<div className="fixed inset-0 z-40" onClick={() => { setCompanySwitcherOpen(false); setConfirmDeleteCompanyId(null); }} /> <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"> <div className="max-h-[240px] overflow-y-auto py-1">
{snapshot.companies.map((company) => ( {snapshot.companies.map((company) => (
<div key={company.id} className="group flex items-center"> <div key={company.id} className="group flex items-center">
{confirmDeleteCompanyId === company.id ? ( {confirmDeleteCompanyId === company.id ? (
<div className="flex w-full items-center gap-2 px-3 py-2"> <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 <button
type="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)} onClick={() => void deleteCompany(company.id)}
> >
Yes Yes
</button> </button>
<button <button
type="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)} onClick={() => setConfirmDeleteCompanyId(null)}
> >
No No
@@ -779,17 +779,17 @@ export function App() {
<> <>
<button <button
type="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)] ${ 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-[color:var(--text)]" : "text-[color:var(--muted-strong)]" company.id === currentCompany?.id ? "font-semibold text-foreground" : "text-muted-foreground"
}`} }`}
onClick={() => { void setCompany(company.id); setCompanySwitcherOpen(false); }} 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> <span className="min-w-0 flex-1 truncate">{company.name}</span>
</button> </button>
<button <button
type="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}`} title={`Delete ${company.name}`}
onClick={() => setConfirmDeleteCompanyId(company.id)} onClick={() => setConfirmDeleteCompanyId(company.id)}
> >
@@ -800,10 +800,10 @@ export function App() {
</div> </div>
))} ))}
</div> </div>
<div className="border-t border-[color:var(--line)]"> <div className="border-t border-border">
<button <button
type="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); }} onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
> >
<Plus size={14} /> <Plus size={14} />
@@ -816,7 +816,7 @@ export function App() {
</div> </div>
<button <button
aria-label="Open command palette" 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)} onClick={() => setCommandPaletteOpen(true)}
> >
<Command size={14} /> <Command size={14} />
@@ -839,7 +839,7 @@ export function App() {
))} ))}
</div> </div>
</nav> </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 <SectionButton
active={section === "settings"} active={section === "settings"}
label="Settings" label="Settings"
@@ -850,7 +850,7 @@ export function App() {
<button <button
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
title={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)} onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
> >
<SidebarSimple size={sidebarCollapsed ? 18 : 17} /> <SidebarSimple size={sidebarCollapsed ? 18 : 17} />
@@ -860,9 +860,9 @@ export function App() {
</aside> </aside>
<main aria-label="Content" className="flex min-w-0 flex-col overflow-hidden"> <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"> <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}`)} {navItems.find((item) => item.id === section)?.label ?? t(`nav.${section}`)}
</h1> </h1>
<div className="flex shrink-0 items-center gap-3 lg:gap-5"> <div className="flex shrink-0 items-center gap-3 lg:gap-5">
@@ -870,7 +870,7 @@ export function App() {
type="button" type="button"
aria-label={consoleOpen ? "Hide console" : "Show console"} aria-label={consoleOpen ? "Hide console" : "Show console"}
title={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)} onClick={() => setConsoleOpen(!consoleOpen)}
> >
<TerminalWindow size={16} /> <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"}> <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 ? ( {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} /> <WarningCircle size={18} />
<span>{error}</span> <span>{error}</span>
</div> </div>
@@ -971,14 +971,14 @@ export function App() {
onClick={() => setSelectedConnectorId(connector.id)} onClick={() => setSelectedConnectorId(connector.id)}
className={`cursor-pointer transition-colors duration-100 ${ className={`cursor-pointer transition-colors duration-100 ${
selectedConnector.id === connector.id selectedConnector.id === connector.id
? "bg-[color:var(--accent-soft)]" ? "bg-accent"
: "hover:bg-[color:var(--panel-soft)]" : "hover:bg-muted"
}`} }`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="min-w-0"> <div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{connector.label}</div> <div className="text-[14px] font-medium text-foreground">{connector.label}</div>
<div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">{connector.command}</div> <div className="mt-1 truncate text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{connector.command}</div>
</div> </div>
<StatusPill status={connector.status} /> <StatusPill status={connector.status} />
</div> </div>
@@ -1123,18 +1123,18 @@ export function App() {
title="Task queue" title="Task queue"
action={ action={
<div className="flex items-center gap-2"> <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 <button
type="button" type="button"
onClick={() => setTaskViewMode("list")} 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} /> <ListChecks size={14} />
</button> </button>
<button <button
type="button" type="button"
onClick={() => setTaskViewMode("kanban")} 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} /> <Kanban size={14} />
</button> </button>
@@ -1213,7 +1213,7 @@ export function App() {
documents={companyDocuments} documents={companyDocuments}
companyId={currentCompany!.id} companyId={currentCompany!.id}
onDecide={async (state, note) => { 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(); await refresh();
}} }}
onDeleted={async () => { setSelectedApprovalId(null); await refresh(); }} onDeleted={async () => { setSelectedApprovalId(null); await refresh(); }}
@@ -1226,7 +1226,7 @@ export function App() {
{section === "costs" ? ( {section === "costs" ? (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Tab bar */} {/* 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) => ( {(["costs", "policies", "incidents"] as const).map((tab) => (
<button <button
key={tab} key={tab}
@@ -1234,8 +1234,8 @@ export function App() {
onClick={() => setCostsTab(tab)} onClick={() => setCostsTab(tab)}
className={`rounded-t px-3 py-1.5 text-[13px] font-medium transition-colors ${ className={`rounded-t px-3 py-1.5 text-[13px] font-medium transition-colors ${
costsTab === tab costsTab === tab
? "border-b-2 border-[color:var(--accent)] text-[color:var(--text)]" ? "border-b-2 border-[color:var(--primary)] text-foreground"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:text-foreground"
}`} }`}
> >
{tab === "costs" ? "Cost Entries" : tab === "policies" ? "Policies" : "Incidents"} {tab === "costs" ? "Cost Entries" : tab === "policies" ? "Policies" : "Incidents"}
@@ -1467,12 +1467,12 @@ export function App() {
animate={{ height: "auto", opacity: 1 }} animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }} exit={{ height: 0, opacity: 0 }}
transition={panelTransition} 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 justify-between px-5 py-2.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<TerminalWindow size={15} className="text-[color:var(--muted)]" /> <TerminalWindow size={15} className="text-muted-foreground" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("console.title")}</span> <span className="text-[13px] font-semibold text-foreground">{t("console.title")}</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Select <Select
@@ -1486,7 +1486,7 @@ export function App() {
/> />
<button <button
type="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)} onClick={() => setConsoleOpen(false)}
aria-label="Close console" aria-label="Close console"
> >
@@ -1494,25 +1494,25 @@ export function App() {
</button> </button>
</div> </div>
</div> </div>
<div className="grid border-t border-[color:var(--line)] xl:grid-cols-[240px_minmax(0,1fr)]"> <div className="grid border-t border-border 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="border-b border-border px-5 py-3 xl:border-b-0 xl:border-r">
{selectedRun ? ( {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"> <div className="flex items-center justify-between">
<span>{t("common.status")}</span> <span>{t("common.status")}</span>
<StatusPill status={selectedRun.status} /> <StatusPill status={selectedRun.status} />
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{t("common.started")}</span> <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>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{t("common.finished")}</span> <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>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{t("common.cost")}</span> <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> </div>
{selectedRun.logPath ? ( {selectedRun.logPath ? (
<div className="pt-1"> <div className="pt-1">
@@ -1521,11 +1521,11 @@ export function App() {
) : null} ) : null}
</div> </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>
<div className="h-[200px] overflow-auto px-5 py-3"> <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")} {selectedRun ? logBufferRef.current[selectedRun.id] ?? t("common.waitingForLog") : t("common.noRunSelected")}
</pre> </pre>
</div> </div>
@@ -1541,7 +1541,7 @@ export function App() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} 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(""); }} onClick={() => { setCommandPaletteOpen(false); setCommandPaletteSearch(""); }}
> >
<motion.div <motion.div
@@ -1553,15 +1553,15 @@ export function App() {
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.98 }} exit={{ opacity: 0, y: 10, scale: 0.98 }}
transition={panelTransition} 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()} onClick={(event) => event.stopPropagation()}
> >
<div className="mb-3 flex items-center gap-3"> <div className="mb-3 flex items-center gap-3">
<MagnifyingGlass size={18} className="text-[color:var(--accent)]" /> <MagnifyingGlass size={18} className="text-primary" />
<input <input
type="text" type="text"
aria-label="Search" 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")} placeholder={t("app.search")}
value={commandPaletteSearch} value={commandPaletteSearch}
onChange={(e) => setCommandPaletteSearch(e.target.value)} onChange={(e) => setCommandPaletteSearch(e.target.value)}
@@ -1569,7 +1569,7 @@ export function App() {
/> />
{commandPaletteSearch && ( {commandPaletteSearch && (
<button <button
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]" className="text-[12px] text-muted-foreground hover:text-foreground"
onClick={() => setCommandPaletteSearch("")} onClick={() => setCommandPaletteSearch("")}
> >
{t("app.clear")} {t("app.clear")}
@@ -1579,12 +1579,12 @@ export function App() {
{filteredNavItems.length > 0 ? ( {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> <ListFrame>
{filteredNavItems.map((item) => ( {filteredNavItems.map((item) => (
<ListRow <ListRow
key={item.id} 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={() => { onClick={() => {
navigateTo(item.id); navigateTo(item.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1593,11 +1593,11 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between"> <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} /> <item.icon size={16} />
<span className="text-[13px] tracking-[0.005em]">{item.label}</span> <span className="text-[13px] tracking-[0.005em]">{item.label}</span>
</div> </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> </div>
</ListRow> </ListRow>
))} ))}
@@ -1609,12 +1609,12 @@ export function App() {
<div className="mt-3 space-y-3"> <div className="mt-3 space-y-3">
{matchedAgents.length > 0 ? ( {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> <ListFrame>
{matchedAgents.map((agent) => ( {matchedAgents.map((agent) => (
<ListRow <ListRow
key={agent.id} 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={() => { onClick={() => {
navigateTo("agents", agent.id); navigateTo("agents", agent.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1623,10 +1623,10 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between"> <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} /> <UsersThree size={16} />
<span className="text-[13px] tracking-[0.005em]">{agent.name}</span> <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> </div>
<StatusPill status={agent.status} /> <StatusPill status={agent.status} />
</div> </div>
@@ -1638,12 +1638,12 @@ export function App() {
{matchedTasks.length > 0 ? ( {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> <ListFrame>
{matchedTasks.map((task) => ( {matchedTasks.map((task) => (
<ListRow <ListRow
key={task.id} 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={() => { onClick={() => {
navigateTo("tasks", task.id); navigateTo("tasks", task.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1652,7 +1652,7 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between"> <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} /> <ListChecks size={16} />
<span className="text-[13px] tracking-[0.005em]">{task.title}</span> <span className="text-[13px] tracking-[0.005em]">{task.title}</span>
</div> </div>
@@ -1666,12 +1666,12 @@ export function App() {
{matchedRuns.length > 0 ? ( {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> <ListFrame>
{matchedRuns.map((run) => ( {matchedRuns.map((run) => (
<ListRow <ListRow
key={run.id} 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={() => { onClick={() => {
navigateTo("runs", run.id); navigateTo("runs", run.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1680,7 +1680,7 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between gap-3"> <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} /> <TerminalWindow size={16} />
<span className="truncate text-[13px] tracking-[0.005em]"> <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)} {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 ? ( {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> <ListFrame>
{matchedGoals.map((goal) => ( {matchedGoals.map((goal) => (
<ListRow <ListRow
key={goal.id} 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={() => { onClick={() => {
navigateTo("goals", goal.id); navigateTo("goals", goal.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1710,7 +1710,7 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between"> <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} /> <Target size={16} />
<span className="text-[13px] tracking-[0.005em]">{goal.title}</span> <span className="text-[13px] tracking-[0.005em]">{goal.title}</span>
</div> </div>
@@ -1724,12 +1724,12 @@ export function App() {
{matchedProjects.length > 0 ? ( {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> <ListFrame>
{matchedProjects.map((project) => ( {matchedProjects.map((project) => (
<ListRow <ListRow
key={project.id} 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={() => { onClick={() => {
navigateTo("projects", project.id); navigateTo("projects", project.id);
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
@@ -1738,7 +1738,7 @@ export function App() {
}} }}
> >
<div className="flex items-center justify-between"> <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} /> <Kanban size={16} />
<span className="text-[13px] tracking-[0.005em]">{project.name}</span> <span className="text-[13px] tracking-[0.005em]">{project.name}</span>
</div> </div>
@@ -1753,7 +1753,7 @@ export function App() {
) : null} ) : null}
{paletteQuery && filteredNavItems.length === 0 && !hasEntityResults ? ( {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} ) : null}
</motion.div> </motion.div>
</motion.div> </motion.div>
@@ -67,15 +67,15 @@ export const AsciiAnimation = memo(function AsciiAnimation() {
{/* Fade masks */} {/* Fade masks */}
<div <div
className="absolute inset-x-0 top-0 z-10 h-32" 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 <div
className="absolute inset-x-0 bottom-0 z-10 h-32" 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 */} {/* 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} {BLOCK}
{"\n"} {"\n"}
{BLOCK} {BLOCK}
@@ -27,54 +27,54 @@ export function ClaudeSettingsDialog({
currentLocale, currentLocale,
}: ClaudeSettingsDialogProps) { }: ClaudeSettingsDialogProps) {
return ( 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="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-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-6 shadow-lg" onClick={(e) => e.stopPropagation()}> <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"> <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> <h3 className="text-[16px] font-semibold text-foreground">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> <button type="button" onClick={onClose} className="rounded-lg p-1 text-muted-foreground transition hover:text-foreground"><X size={16} /></button>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div> <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 <input
type="password" 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} value={claudeApiKey}
onChange={(e) => setClaudeApiKey(e.target.value)} onChange={(e) => setClaudeApiKey(e.target.value)}
placeholder="sk-ant-..." 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>
<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 <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} value={claudeBaseUrl}
onChange={(e) => setClaudeBaseUrl(e.target.value)} onChange={(e) => setClaudeBaseUrl(e.target.value)}
placeholder="https://api.anthropic.com" 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>
<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 <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} value={claudeModel}
onChange={(e) => setClaudeModel(e.target.value)} onChange={(e) => setClaudeModel(e.target.value)}
placeholder="claude-sonnet-4-20250514" 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> </div>
<div className="mt-6 flex justify-end gap-3"> <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"} {currentLocale === "zh" ? "取消" : "Cancel"}
</button> </button>
<button <button
type="button" type="button"
onClick={onSave} onClick={onSave}
disabled={saving} 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")} {saving ? (currentLocale === "zh" ? "保存中..." : "Saving...") : (currentLocale === "zh" ? "保存" : "Save")}
</button> </button>
@@ -58,15 +58,15 @@ export function CompanyStep({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }} exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }} 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 {t("onboarding.stepLabel1")} 1 / 3
</div> </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")} {t("onboarding.step1Title")}
</div> </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"> <div className="space-y-4">
<Input <Input
@@ -91,15 +91,15 @@ export function CompanyStep({
label: connector.label, 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")} {t("onboarding.connectorInfo")}
</div> </div>
<div> <div>
<label <label
htmlFor="onboarding-workspace-path" 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> </label>
<div className="flex gap-3"> <div className="flex gap-3">
<Input <Input
@@ -111,37 +111,37 @@ export function CompanyStep({
/> />
<ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={onPickDirectory} icon={FolderOpen} /> <ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={onPickDirectory} icon={FolderOpen} />
</div> </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")} {t("onboarding.workspaceHelp")}
</div> </div>
</div> </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"> <div className="grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
onClick={() => { setReviewDeliverables(false); setAutoApproveHires(true) }} 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)]"} /> <Lightning size={20} weight={!reviewDeliverables ? "fill" : "regular"} className={!reviewDeliverables ? "text-primary" : "text-muted-foreground"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeFullAuto")}</div> <div className="mt-2 text-[13px] font-semibold text-foreground">{t("onboarding.modeFullAuto")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeFullAutoDesc")}</div> <div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("onboarding.modeFullAutoDesc")}</div>
</button> </button>
<button <button
type="button" type="button"
onClick={() => { setReviewDeliverables(true); setAutoApproveHires(false) }} 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)]"} /> <UserCircleCheck size={20} weight={reviewDeliverables ? "fill" : "regular"} className={reviewDeliverables ? "text-primary" : "text-muted-foreground"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeHumanReview")}</div> <div className="mt-2 text-[13px] font-semibold text-foreground">{t("onboarding.modeHumanReview")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeHumanReviewDesc")}</div> <div className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{t("onboarding.modeHumanReviewDesc")}</div>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{error ? ( {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} {error}
</div> </div>
) : null} ) : null}
@@ -151,7 +151,7 @@ export function CompanyStep({
<button <button
type="button" type="button"
onClick={onBack} 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")} {t("onboarding.back")}
</button> </button>
@@ -25,7 +25,7 @@ export function ConnectorReadiness({
}: ConnectorReadinessProps) { }: ConnectorReadinessProps) {
return ( return (
<div className="mt-10"> <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")} {t("onboarding.connectorReadiness")}
</div> </div>
<div className="space-y-2.5"> <div className="space-y-2.5">
@@ -37,25 +37,25 @@ export function ConnectorReadiness({
return ( return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]"> <div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isChecking ? ( {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 ? ( ) : 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 ? ( ) : 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="min-w-0 flex-1">
<div className="flex items-center gap-2"> <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} {connector.label}
</span> </span>
<span className="text-[11px] text-[color:var(--muted)]"> <span className="text-[11px] text-muted-foreground">
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")} {isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span> </span>
</div> </div>
{issue ? ( {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} {issue}
</div> </div>
) : null} ) : null}
@@ -64,7 +64,7 @@ export function ConnectorReadiness({
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); onOpenClaudeSettings(); }} 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"} title={currentLocale === "zh" ? "Claude Code 设置" : "Claude Code Settings"}
> >
<GearSix size={14} /> <GearSix size={14} />
@@ -75,7 +75,7 @@ export function ConnectorReadiness({
})} })}
</div> </div>
{!hasRunnableConnector ? ( {!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")} {t("onboarding.connectorWarning")}
</div> </div>
) : null} ) : null}
@@ -24,16 +24,16 @@ export function LanguageStep({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }} exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }} 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} /> <GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language {t("onboarding.languageSelection")} / Language
</div> </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 {t("onboarding.selectLanguage")} / Select Language
</div> </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"> <div className="space-y-2.5">
{APP_LOCALES.map((locale) => ( {APP_LOCALES.map((locale) => (
@@ -41,21 +41,21 @@ export function LanguageStep({
key={locale} key={locale}
type="button" type="button"
onClick={() => onLocaleChange(locale)} 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 currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]" ? "border-[color:var(--primary)] bg-accent"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]" : "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 ${ <div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent)]" ? "border-[color:var(--primary)] bg-primary"
: "border-[color:var(--line)]" : "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> </div>
<span className={`text-[14px] font-medium ${ <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]} {LOCALE_LABELS[locale]}
</span> </span>
@@ -26,53 +26,53 @@ export function LaunchStep({
initial={{ opacity: 0, scale: 0.96 }} initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 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" /> <CheckCircle size={24} weight="fill" />
</div> </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")} {launchSummary.companyName} {t("onboarding.isLive")}
</h2> </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")} {launchSummary.runId ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
</p> </p>
<div className="mt-6 grid gap-3 md:grid-cols-2"> <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="rounded-lg border border-border bg-muted px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div> <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-[color:var(--text)]"> <div className="mt-1 text-[13px] font-medium text-foreground">
{selectedConnector?.label ?? t("onboarding.unavailable")} {selectedConnector?.label ?? t("onboarding.unavailable")}
</div> </div>
</div> </div>
<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">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div> <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-[color:var(--text)]"> <div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")} {launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div> </div>
</div> </div>
<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">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div> <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-[color:var(--text)]"> <div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")} {launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div> </div>
</div> </div>
<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">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div> <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-[color:var(--text)]"> <div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")} {launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div> </div>
</div> </div>
<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">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div> <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-[color:var(--text)]"> <div className="mt-1 text-[13px] font-medium text-foreground">
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")} {launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
</div> </div>
</div> </div>
</div> </div>
{error ? ( {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} {error}
</div> </div>
) : null} ) : null}
@@ -32,15 +32,15 @@ export function MissionStep({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }} exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }} 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 {t("onboarding.stepLabel2")} 2 / 3
</div> </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")} {t("onboarding.step2Title")}
</div> </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"> <div className="space-y-4">
<Input <Input
@@ -56,8 +56,8 @@ export function MissionStep({
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")} placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
rows={5} 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="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-[color:var(--muted)]"> <div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground">
<Cpu size={12} /> <Cpu size={12} />
{t("onboarding.executionHandoff")} {t("onboarding.executionHandoff")}
</div> </div>
@@ -70,7 +70,7 @@ export function MissionStep({
</div> </div>
{error ? ( {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} {error}
</div> </div>
) : null} ) : null}
@@ -79,7 +79,7 @@ export function MissionStep({
<button <button
type="button" type="button"
onClick={onBack} 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")} {t("onboarding.back")}
</button> </button>
@@ -47,13 +47,13 @@ export function OnboardingHero({
<div <div
className="pointer-events-none absolute inset-0 opacity-[0.035]" className="pointer-events-none absolute inset-0 opacity-[0.035]"
style={{ 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 <div
className="pointer-events-none absolute inset-0 opacity-[0.025]" className="pointer-events-none absolute inset-0 opacity-[0.025]"
style={{ 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", animation: "hero-drift 12s ease-in-out infinite alternate",
}} }}
/> />
@@ -77,10 +77,10 @@ export function OnboardingHero({
<div <div
className="h-px flex-1 max-w-[32px]" className="h-px flex-1 max-w-[32px]"
style={{ 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")} {t("onboarding.subtitle")}
</span> </span>
</motion.div> </motion.div>
@@ -90,7 +90,7 @@ export function OnboardingHero({
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15, ease: [0.16, 1, 0.3, 1] }} 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) => ( {t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span> <span key={i}>{line}{i === 0 ? <br /> : null}</span>
@@ -102,7 +102,7 @@ export function OnboardingHero({
initial={{ opacity: 0, y: 8 }} initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.25, ease: [0.16, 1, 0.3, 1] }} 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")} {t("onboarding.description")}
</motion.p> </motion.p>
@@ -118,15 +118,15 @@ export function OnboardingHero({
{stepLabels.map((entry, index) => ( {stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2"> <div key={entry.label} className="flex items-center gap-2">
{index > 0 ? ( {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} ) : null}
<div <div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium transition-colors duration-300 ${ className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium transition-colors duration-300 ${
index < stepIndex 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 : index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" ? "bg-accent text-primary"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" : "bg-muted text-muted-foreground"
}`} }`}
> >
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />} {index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
@@ -156,13 +156,13 @@ export function OnboardingHero({
<div <div
className="h-px w-8" className="h-px w-8"
style={{ style={{
background: "linear-gradient(90deg, var(--accent), transparent)", background: "linear-gradient(90deg, var(--primary), transparent)",
}} }}
/> />
<span <span
className="select-none text-[10px] font-semibold uppercase tracking-[0.2em]" className="select-none text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{ style={{
background: "linear-gradient(135deg, var(--accent) 0%, var(--muted) 100%)", background: "linear-gradient(135deg, var(--primary) 0%, var(--muted-foreground) 100%)",
WebkitBackgroundClip: "text", WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent", WebkitTextFillColor: "transparent",
backgroundClip: "text", backgroundClip: "text",
@@ -187,7 +187,7 @@ export function CompanyOnboarding({
}, [launchSummary, onDone, t]); }, [launchSummary, onDone, t]);
return ( 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 */} {/* Drag region for macOS frameless title bar */}
<div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" /> <div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" />
<OnboardingHero <OnboardingHero
@@ -199,13 +199,13 @@ export function CompanyOnboarding({
t={t} 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"> <div className="relative flex min-h-full items-center px-6 py-10 md:px-10">
{canCancel && onCancel ? ( {canCancel && onCancel ? (
<button <button
type="button" type="button"
onClick={onCancel} 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")} {t("onboarding.cancel")}
</button> </button>
+2 -2
View File
@@ -14,8 +14,8 @@ export function EntityWorkspace({
return ( return (
<div className="space-y-14"> <div className="space-y-14">
<div> <div>
{eyebrow ? <div className="text-[10px] uppercase tracking-[0.08em] text-[color:var(--muted)]">{eyebrow}</div> : null} {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-[color:var(--text)]`}>{title}</h2> <h2 className={`${eyebrow ? "mt-2" : ""} text-[18px] font-semibold tracking-[-0.01em] text-foreground`}>{title}</h2>
</div> </div>
<div className="space-y-14">{primary}{secondary}</div> <div className="space-y-14">{primary}{secondary}</div>
</div> </div>
+4 -4
View File
@@ -4,9 +4,9 @@ import { useAppStore } from "../state/store"
import { useReducedMotion } from "./ui" import { useReducedMotion } from "./ui"
const TONE_CONFIG = { const TONE_CONFIG = {
success: { icon: CheckCircle, bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" }, success: { icon: CheckCircle, bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" },
danger: { icon: XCircle, bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" }, danger: { icon: XCircle, bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" },
warn: { icon: Warning, bg: "var(--warn-soft)", border: "var(--warn)", text: "var(--warn)" }, warn: { icon: Warning, bg: "var(--muted-foreground)", border: "var(--destructive)", text: "var(--destructive)" },
} as const } as const
export default function ToastContainer() { export default function ToastContainer() {
@@ -29,7 +29,7 @@ export default function ToastContainer() {
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.96 }} exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.96 }}
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }} 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 }} style={{ backgroundColor: cfg.bg, borderColor: cfg.border }}
> >
<Icon size={16} weight="fill" className="mt-px shrink-0" style={{ color: cfg.text }} /> <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 if (!open) return null
return ( 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={{ 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()}> <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-[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> <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-[color:var(--muted)]">Are you sure you want to delete <strong className="text-[color:var(--text)]">{entityName}</strong>? This action cannot be undone.</p> <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-[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> <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>
</motion.div> </motion.div>
) )
} }
function AutomationActivityLog({ entries }: { entries: AutomationLogEntry[] }) { 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 ( return (
<div className="space-y-1">{entries.map((entry, idx) => { <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 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> })}</div>
) )
} }
@@ -47,14 +47,14 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
return ( return (
<div className="space-y-5"> <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} {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-[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> <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(--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} {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-[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> <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" ? <> {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} {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-[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 && 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-[color:var(--muted)]">No rules match the current filter.</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>} : <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} />} </> : <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> <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]) const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows])
return ( return (
<div className="space-y-5"> <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} {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-[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> <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-[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> {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>} : <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>{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> <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") const t = useT(); const [activeSection, setActiveSection] = useState<"rules"|"workflows">("rules")
return ( return (
<div className="space-y-6"> <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="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-[8px] bg-[color:var(--panel-soft)] p-1"> <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-[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("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-[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> <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> </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> <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> </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] const TriggerIcon = triggerInfo?.icon ?? Lightning; const colors = statusColors[rule.status]
return ( 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 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"> <button type="button" onClick={onEdit} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2"> <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> <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>
<div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[color:var(--muted)]"> <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-[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> <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-[color:var(--muted)]" /> <ArrowRight size={10} className="text-muted-foreground" />
<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> <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(--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} {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>
<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> </button>
<div className="flex shrink-0 items-center gap-1"> <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={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-[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={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>
</div> </div>
</motion.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] const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action]
return ( 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={{ 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 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> <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-[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> <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>
<div className="overflow-y-auto px-6 py-5"><div className="space-y-5"> <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} {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-[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-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-[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-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-[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-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-[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-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-[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-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-[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><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-[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 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-[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> <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></div>
<div className="flex shrink-0 justify-end gap-3 border-t px-6 py-4" style={{ borderColor: LINE }}> <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()} /> <ActionButton label={isEditing ? "Save Changes" : "Create Rule"} onClick={() => void handleSave()} icon={isEditing ? Gear : Lightning} tone="accent" loading={saving} disabled={!form.name.trim()} />
</div> </div>
</motion.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" }] const statusOptions = [{ value: "draft", label: "Draft" }, { value: "active", label: "Active" }, { value: "paused", label: "Paused" }]
return ( 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={{ 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()}> <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-[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> <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"> <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} {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-[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-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-[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><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-[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 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-[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-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-[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> <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></div>
<div className="flex shrink-0 justify-end gap-3 border-t px-6 py-4" style={{ borderColor: LINE }}> <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()} /> <ActionButton label={isEditing ? "Save Changes" : "Create Workflow"} onClick={() => void handleSave()} icon={isEditing ? Gear : ArrowsClockwise} tone="accent" loading={saving} disabled={!form.name.trim()} />
</div> </div>
</motion.div> </motion.div>
@@ -7,19 +7,19 @@ import { ActionButton } from "../ui"
import { ACCENT, LINE, triggerConfig, actionConfig, workflowStatusColors, stepStatusConfig, safeJsonParse } from "./automation-config" import { ACCENT, LINE, triggerConfig, actionConfig, workflowStatusColors, stepStatusConfig, safeJsonParse } from "./automation-config"
function StepTimeline({ steps }: { steps: WorkflowStep[] }) { 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 ( return (
<div className="relative pl-5"> <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"> <div className="space-y-3">
{steps.map((step, idx) => { {steps.map((step, idx) => {
const colors = stepStatusConfig[step.status]; const info = actionConfig[step.action] const colors = stepStatusConfig[step.status]; const info = actionConfig[step.action]
return ( return (
<div key={step.id} className="relative flex items-start gap-3"> <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="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-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-[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="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-[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="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>
</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]) const steps: WorkflowStep[] = useMemo(() => { const parsed = safeJsonParse(workflow.stepsJson); return Array.isArray(parsed) ? (parsed as WorkflowStep[]) : [] }, [workflow.stepsJson])
return ( 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 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"> <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="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-[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="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> </button>
<div className="flex shrink-0 items-center gap-1"> <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> <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-[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={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-[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={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-[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={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>
</div> </div>
<AnimatePresence> <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> </AnimatePresence>
</motion.div> </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 { ArrowsClockwise, Eye, Funnel, Lightning, Pause, Play, Plus, Robot, Timer, Trash } from "@phosphor-icons/react"
import { DEPARTMENT_OPTIONS } from "@shared/constants" import { DEPARTMENT_OPTIONS } from "@shared/constants"
export const ACCENT = "var(--accent)" export const ACCENT = "var(--primary)"
export const LINE = "var(--line)" export const LINE = "var(--border)"
export const triggerConfig: Record<AutomationTrigger, { label: string; description: string; icon: typeof Lightning }> = { 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 }, 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 departmentOptions = DEPARTMENT_OPTIONS
export const statusColors: Record<AutomationStatus, { bg: string; text: string; dot: string }> = { export const statusColors: Record<AutomationStatus, { bg: string; text: string; dot: string }> = {
active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, active: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" }, paused: { bg: "var(--muted-foreground)", text: "var(--destructive)", dot: "var(--destructive)" },
disabled: { bg: "var(--panel-soft)", text: "var(--muted)", dot: "var(--muted)" }, disabled: { bg: "var(--muted-foreground)", text: "var(--muted-foreground)", dot: "var(--muted-foreground)" },
error: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" }, error: { bg: "var(--destructive)", text: "var(--destructive)", dot: "var(--destructive)" },
} }
export const workflowStatusColors: Record<WorkflowStatus, { bg: string; text: string; dot: string }> = { export const workflowStatusColors: Record<WorkflowStatus, { bg: string; text: string; dot: string }> = {
draft: { bg: "var(--panel-soft)", text: "var(--muted-strong)", dot: "var(--muted)" }, draft: { bg: "var(--muted-foreground)", text: "var(--muted-foreground)", dot: "var(--muted-foreground)" },
active: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, active: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
paused: { bg: "var(--warn-soft)", text: "var(--warn)", dot: "var(--warn)" }, paused: { bg: "var(--muted-foreground)", text: "var(--destructive)", dot: "var(--destructive)" },
completed: { bg: "var(--success-soft)", text: "var(--success)", dot: "var(--success)" }, completed: { bg: "var(--muted-foreground)", text: "var(--primary)", dot: "var(--primary)" },
failed: { bg: "var(--danger-soft)", text: "var(--danger)", dot: "var(--danger)" }, failed: { bg: "var(--destructive)", text: "var(--destructive)", dot: "var(--destructive)" },
} }
export const stepStatusConfig: Record<WorkflowStep["status"], { bg: string; border: string; text: string }> = { export const stepStatusConfig: Record<WorkflowStep["status"], { bg: string; border: string; text: string }> = {
pending: { 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(--accent-soft)", border: ACCENT, text: ACCENT }, running: { bg: "var(--primary)", border: ACCENT, text: ACCENT },
completed: { bg: "var(--success-soft)", border: "var(--success)", text: "var(--success)" }, completed: { bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" },
failed: { bg: "var(--danger-soft)", border: "var(--danger)", text: "var(--danger)" }, failed: { bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" },
skipped: { bg: "var(--panel-soft)", border: "var(--line)", text: "var(--muted)" }, 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 } export interface AutomationLogEntry { ruleId: string; ruleName: string; trigger: string; action: string; executedAt: string }
@@ -47,12 +47,12 @@ export function ChannelHeader({
const t = useT() const t = useT()
return ( 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 ? ( {!showSidebar ? (
<button <button
type="button" type="button"
onClick={onShowSidebar} 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" aria-label="Show sidebar"
> >
<CaretDown size={12} className="rotate-90" /> <CaretDown size={12} className="rotate-90" />
@@ -61,15 +61,15 @@ export function ChannelHeader({
{activeChannel ? ( {activeChannel ? (
<div className="min-w-0 flex-1"> <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 ? ( {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 Open task
</button> </button>
) : null} ) : null}
</div> </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]"> <div className="ml-auto w-full max-w-[320px]">
@@ -7,11 +7,11 @@ import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeade
import { useT } from "../../i18n" import { useT } from "../../i18n"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = { const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--accent)" }, direct: { label: "Direct Messages", icon: EnvelopeSimple, color: "var(--primary)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" }, department: { label: "Department", icon: Buildings, color: "var(--muted-foreground)" },
company: { label: "Company-wide", icon: Megaphone, color: "var(--accent)" }, company: { label: "Company-wide", icon: Megaphone, color: "var(--primary)" },
project: { label: "Project", icon: Hash, color: "var(--success)" }, project: { label: "Project", icon: Hash, color: "var(--primary)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" }, incident: { label: "Incident", icon: Fire, color: "var(--destructive)" },
} }
function ChannelItem({ function ChannelItem({
@@ -38,15 +38,15 @@ function ChannelItem({
type="button" type="button"
onClick={onClick} onClick={onClick}
aria-current={active ? "page" : undefined} 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 ${ className={`group relative flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left transition duration-150 ${
active ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]/50" active ? "bg-accent" : "hover:bg-muted/50"
}`} }`}
> >
{descriptor.channel === "direct" && agent ? ( {descriptor.channel === "direct" && agent ? (
<AgentAvatar agent={agent} size="sm" /> <AgentAvatar agent={agent} size="sm" />
) : ( ) : (
<div <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` }} style={{ backgroundColor: `${config.color}18` }}
> >
<Icon size={14} weight={active ? "fill" : "regular"} style={{ color: config.color }} /> <Icon size={14} weight={active ? "fill" : "regular"} style={{ color: config.color }} />
@@ -56,21 +56,21 @@ function ChannelItem({
<span <span
className={`min-w-0 flex-1 truncate text-[13px] ${ className={`min-w-0 flex-1 truncate text-[13px] ${
active active
? "font-semibold text-[color:var(--text)]" ? "font-semibold text-foreground"
: unreadCount > 0 : unreadCount > 0
? "font-semibold text-[color:var(--text)]" ? "font-semibold text-foreground"
: "font-medium text-[color:var(--muted-strong)]" : "font-medium text-muted-foreground"
}`} }`}
> >
{descriptor.label} {descriptor.label}
</span> </span>
{hasUrgent ? ( {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 ? ( ) : unreadCount > 0 ? (
<span <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)]" 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(--accent)" }} style={{ backgroundColor: "var(--primary)" }}
> >
{unreadCount > 99 ? "99+" : unreadCount} {unreadCount > 99 ? "99+" : unreadCount}
</span> </span>
@@ -108,13 +108,13 @@ export function ChannelSidebar({
animate={{ width: 260, opacity: 1 }} animate={{ width: 260, opacity: 1 }}
exit={{ width: 0, opacity: 0 }} exit={{ width: 0, opacity: 0 }}
transition={{ duration: 0.2 }} 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 }} style={{ width: 260 }}
> >
<div className="flex-1 overflow-y-auto px-2 py-3"> <div className="flex-1 overflow-y-auto px-2 py-3">
{channelGroups.map((group) => ( {channelGroups.map((group) => (
<div key={group.type} className="mb-3"> <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} {group.label}
</div> </div>
{group.items.map((ch) => { {group.items.map((ch) => {
@@ -136,7 +136,7 @@ export function ChannelSidebar({
{taskDiscussionChannels.length > 0 ? ( {taskDiscussionChannels.length > 0 ? (
<div className="mb-3"> <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")} {t("comm.taskDiscussions")}
</div> </div>
{taskDiscussionChannels.map((discussion) => ( {taskDiscussionChannels.map((discussion) => (
@@ -144,14 +144,14 @@ export function ChannelSidebar({
key={discussion.key} key={discussion.key}
type="button" type="button"
onClick={() => onSelectChannel(discussion.key)} 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 ${ 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-[color:var(--panel-soft)]" : "hover:bg-[color:var(--panel-soft)]/50" 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)" }}> <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(--accent)" }} /> <Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--primary)" }} />
</div> </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} {discussion.label}
</span> </span>
</button> </button>
@@ -160,14 +160,14 @@ export function ChannelSidebar({
) : null} ) : null}
{totalChannels === 0 ? ( {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")} {t("comm.noActiveDepartments")}
</div> </div>
) : null} ) : null}
</div> </div>
<div className="border-t border-[color:var(--line)] px-4 py-2"> <div className="border-t border-border px-4 py-2">
<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>{totalChannels} channels</span> <span>{totalChannels} channels</span>
<span>{agents.length} agents</span> <span>{agents.length} agents</span>
</div> </div>
@@ -201,29 +201,29 @@ export function FilterBar({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative min-w-0 flex-1"> <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 <input
type="text" type="text"
value={filters.search} value={filters.search}
onChange={(e) => onChangeFilters({ ...filters, search: e.target.value })} onChange={(e) => onChangeFilters({ ...filters, search: e.target.value })}
onKeyDown={onSearchKeyDown} onKeyDown={onSearchKeyDown}
placeholder={t("comm.searchMessages")} 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" 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(--accent)" } as React.CSSProperties} style={{ focusRingColor: "var(--primary)" } as React.CSSProperties}
/> />
</div> </div>
{scopeToggle} {scopeToggle}
<button <button
type="button" type="button"
onClick={() => setExpanded(!expanded)} 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 expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? "border-transparent text-[color:var(--text-on-accent)]" ? "border-transparent text-primary-foreground"
: "border-[color:var(--line)] text-[color:var(--muted-strong)] hover:border-[color:var(--line-strong)]" : "border-border text-muted-foreground hover:border-border"
}`} }`}
style={ style={
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? { backgroundColor: "var(--accent)" } ? { backgroundColor: "var(--primary)" }
: undefined : undefined
} }
> >
@@ -243,7 +243,7 @@ export function FilterBar({
<select <select
value={filters.channelType} value={filters.channelType}
onChange={(e) => onChangeFilters({ ...filters, channelType: e.target.value as MessageChannel | "all" })} 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="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option> <option value="direct">{t("comm.direct")}</option>
@@ -255,7 +255,7 @@ export function FilterBar({
<select <select
value={filters.priority} value={filters.priority}
onChange={(e) => onChangeFilters({ ...filters, priority: e.target.value })} 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="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option> <option value="urgent">{t("comm.urgent")}</option>
@@ -266,9 +266,9 @@ export function FilterBar({
type="button" type="button"
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })} onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${ 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")} {t("comm.unreadOnly")}
</button> </button>
@@ -276,7 +276,7 @@ export function FilterBar({
<button <button
type="button" type="button"
onClick={() => onChangeFilters({ search: filters.search, channelType: "all", priority: "all", unreadOnly: false })} 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")} {t("comm.clearFilters")}
</button> </button>
@@ -22,7 +22,7 @@ interface CommunicationHubProps {
function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channel"; onChangeScope: (scope: "global" | "channel") => void }) { function SearchScopeToggle({ scope, onChangeScope }: { scope: "global" | "channel"; onChangeScope: (scope: "global" | "channel") => void }) {
return ( return (
<div <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" role="radiogroup"
aria-label="Search scope" 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") } }} 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 ${ className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "global" scope === "global"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]" ? "bg-primary text-primary-foreground"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]" : "bg-muted text-muted-foreground"
}`} }`}
> >
All Channels 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") } }} 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 ${ className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition-colors duration-150 ${
scope === "channel" scope === "channel"
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]" ? "bg-primary text-primary-foreground"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]" : "bg-muted text-muted-foreground"
}`} }`}
> >
This Channel This Channel
@@ -9,7 +9,7 @@ import { useT } from "../../i18n"
function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) { function SendButton({ sending, disabled, onClick, label }: { sending: boolean; disabled: boolean; onClick: () => void; label: string }) {
return ( 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" />} {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> </button>
) )
@@ -49,30 +49,30 @@ export function MessageComposer({ agents, companyId, channel, channelTargetId, d
const fromAgent = agents.find((a) => a.id === fromAgentId) const fromAgent = agents.find((a) => a.id === fromAgentId)
return ( 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} {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} {channel === "direct" && !directRecipientId ? <div className="mb-2"><InlineNotice message="Select a direct conversation partner before sending a DM." /></div> : null}
<AnimatePresence> <AnimatePresence>
{replyTo ? ( {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"> <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-[color:var(--accent)]" /> <ArrowBendUpLeft size={12} className="text-primary" />
<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> <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-[color:var(--muted)] transition hover:text-[color:var(--text)]"><X size={12} /></button> <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> </motion.div>
) : null} ) : null}
</AnimatePresence> </AnimatePresence>
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<AgentAvatar agent={fromAgent} size="sm" /> <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>)} {agents.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.role})</option>)}
</select> </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>
<div className="flex items-end gap-2"> <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" /> <SendButton sending={sending} disabled={!body.trim() || !fromAgentId || (channel === "direct" && !directRecipientId)} onClick={() => void handleSend()} label="Send message" />
</div> </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> </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) } 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]) }, [body, companyId, taskId, onSent])
return ( 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} {sendError ? <div className="mb-3"><InlineNotice message={sendError} /></div> : null}
<div className="flex items-end gap-2"> <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" /> <SendButton sending={sending} disabled={!body.trim()} onClick={() => void handleSend()} label="Send task comment" />
</div> </div>
</div> </div>
@@ -96,7 +96,7 @@ export function TaskDiscussionComposer({ companyId, taskId, onSent }: { companyI
export function NoChannelComposer() { export function NoChannelComposer() {
const t = useT() 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 }) { 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} /> <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" }]} /> <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"> <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={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-[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={() => 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")} {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> </button>
</div> </div>
@@ -14,14 +14,14 @@ import { DEPARTMENT_COLORS } from "@shared/constants"
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = { const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
urgent: { label: "Urgent", color: "var(--danger)", bg: "var(--danger-soft)" }, urgent: { label: "Urgent", color: "var(--destructive)", bg: "var(--destructive)" },
normal: { label: "Normal", color: "var(--muted-strong)", bg: "var(--panel-soft)" }, normal: { label: "Normal", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
low: { label: "Low", color: "var(--muted)", bg: "var(--panel-soft)" }, low: { label: "Low", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
} }
function getDepartmentColor(department: DepartmentId | null | undefined): string { function getDepartmentColor(department: DepartmentId | null | undefined): string {
if (!department) return "var(--muted)" if (!department) return "var(--muted-foreground)"
return DEPARTMENT_COLORS[department] ?? "var(--muted)" return DEPARTMENT_COLORS[department] ?? "var(--muted-foreground)"
} }
export const AgentAvatar = memo(function AgentAvatar({ export const AgentAvatar = memo(function AgentAvatar({
@@ -32,7 +32,7 @@ export const AgentAvatar = memo(function AgentAvatar({
size?: "sm" | "md" | "lg" 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 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) : "?" const initials = agent ? getAgentInitials(agent.name) : "?"
return ( return (
@@ -60,19 +60,19 @@ export const MessageBubble = memo(function MessageBubble({
}) { }) {
const isUnread = !message.readAt const isUnread = !message.readAt
const isUrgent = message.priority === "urgent" const isUrgent = message.priority === "urgent"
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted)" const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted-foreground)"
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 6 }} initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }} 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" : "" isThread ? "ml-11" : ""
} ${ } ${
isUnread isUnread
? "bg-[color:var(--panel)]" ? "bg-card"
: "hover:bg-[color:var(--panel-soft)]/40" : "hover:bg-muted/40"
}`} }`}
> >
<AgentAvatar agent={agent} /> <AgentAvatar agent={agent} />
@@ -83,7 +83,7 @@ export const MessageBubble = memo(function MessageBubble({
{agent?.name ?? "Unknown Agent"} {agent?.name ?? "Unknown Agent"}
</span> </span>
{agent?.title ? ( {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} ) : null}
{agent?.department ? ( {agent?.department ? (
<span <span
@@ -93,7 +93,7 @@ export const MessageBubble = memo(function MessageBubble({
{agent.department.replace("_", " ")} {agent.department.replace("_", " ")}
</span> </span>
) : null} ) : 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)} {formatMessageTime(message.createdAt)}
</span> </span>
{message.priority !== "normal" ? ( {message.priority !== "normal" ? (
@@ -109,23 +109,23 @@ export const MessageBubble = memo(function MessageBubble({
</span> </span>
) : null} ) : null}
{isUnread ? ( {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 <CheckCircle
size={10} size={10}
weight="fill" 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> </div>
{message.subject ? ( {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} {message.subject}
</div> </div>
) : null} ) : 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} {message.body}
</div> </div>
@@ -139,7 +139,7 @@ export const MessageBubble = memo(function MessageBubble({
type="button" type="button"
onClick={onReply} onClick={onReply}
className="flex items-center gap-1.5 text-[11px] font-medium transition hover:opacity-80" 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} /> <ChatCircle size={12} />
{replyCount} {replyCount === 1 ? "reply" : "replies"} {replyCount} {replyCount === 1 ? "reply" : "replies"}
@@ -148,7 +148,7 @@ export const MessageBubble = memo(function MessageBubble({
<button <button
type="button" type="button"
onClick={onReply} 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} /> <ArrowBendUpLeft size={10} />
Reply Reply
@@ -167,25 +167,25 @@ export function TaskCommentBubble({
agent: AgentRecord | undefined agent: AgentRecord | undefined
}) { }) {
const isBoard = !comment.authorAgentId const isBoard = !comment.authorAgentId
const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted)" const deptColor = agent ? getDepartmentColor(agent.department) : "var(--muted-foreground)"
return ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 6 }} initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.18 }} 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} /> <AgentAvatar agent={agent} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <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} {comment.authorName}
</span> </span>
{agent?.title ? <span className="truncate text-[11px] text-[color:var(--muted)]">{agent.title}</span> : null} {agent?.title ? <span className="truncate text-[11px] text-muted-foreground">{agent.title}</span> : null}
<span className="whitespace-nowrap shrink-0 text-[11px] text-[color:var(--muted)]">{formatMessageTime(comment.createdAt)}</span> <span className="whitespace-nowrap shrink-0 text-[11px] text-muted-foreground">{formatMessageTime(comment.createdAt)}</span>
</div> </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} {comment.body}
</div> </div>
</div> </div>
@@ -207,9 +207,9 @@ function AttachmentList({ json }: { json: string }) {
{items.map((item, i) => ( {items.map((item, i) => (
<span <span
key={`${item.id}-${i}`} 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)}`} {item.title || `${item.type}:${item.id.slice(0, 8)}`}
</span> </span>
))} ))}
@@ -92,8 +92,8 @@ export function MessageList({
if (!activeChannel) { if (!activeChannel) {
return ( return (
<div className="flex h-full flex-col items-center justify-center text-center"> <div className="flex h-full flex-col items-center justify-center text-center">
<ChatCircle size={40} className="mb-3 text-[color:var(--muted)]" /> <ChatCircle size={40} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]"> <div className="text-[14px] font-medium text-muted-foreground">
{t("comm.selectChannelView")} {t("comm.selectChannelView")}
</div> </div>
</div> </div>
@@ -103,8 +103,8 @@ export function MessageList({
if (activeChannel.kind === "task" && taskDiscussionComments.length === 0) { if (activeChannel.kind === "task" && taskDiscussionComments.length === 0) {
return ( return (
<div className="flex h-full flex-col items-center justify-center text-center"> <div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" /> <Tray size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]"> <div className="text-[14px] font-medium text-muted-foreground">
{t("comm.noDiscussionYet")} {t("comm.noDiscussionYet")}
</div> </div>
</div> </div>
@@ -114,14 +114,14 @@ export function MessageList({
if (activeChannel.kind !== "task" && filteredMessages.length === 0) { if (activeChannel.kind !== "task" && filteredMessages.length === 0) {
return ( return (
<div className="flex h-full flex-col items-center justify-center text-center"> <div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" /> <Tray size={36} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]"> <div className="text-[14px] font-medium text-muted-foreground">
{filters.search || filters.unreadOnly || filters.priority !== "all" {filters.search || filters.unreadOnly || filters.priority !== "all"
? t("comm.noMatchFilters") ? t("comm.noMatchFilters")
: t("comm.noMessagesChannel")} : t("comm.noMessagesChannel")}
</div> </div>
{(filters.search || filters.unreadOnly || filters.priority !== "all") ? ( {(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} ) : null}
</div> </div>
) )
@@ -159,7 +159,7 @@ export function MessageList({
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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" ? ( {item.type === "comment" ? (
<TaskCommentBubble <TaskCommentBubble
@@ -9,11 +9,11 @@ import { EmptyState, useReducedMotion } from "../ui"
import { useRovingTabIndex } from "../../lib/keyboard" import { useRovingTabIndex } from "../../lib/keyboard"
const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = { const channelConfig: Record<MessageChannel, { label: string; icon: React.ElementType; color: string }> = {
direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--accent)" }, direct: { label: "Direct", icon: EnvelopeSimple, color: "var(--primary)" },
department: { label: "Department", icon: Buildings, color: "var(--muted-strong)" }, department: { label: "Department", icon: Buildings, color: "var(--muted-foreground)" },
company: { label: "Company", icon: Megaphone, color: "var(--accent)" }, company: { label: "Company", icon: Megaphone, color: "var(--primary)" },
project: { label: "Project", icon: Hash, color: "var(--success)" }, project: { label: "Project", icon: Hash, color: "var(--primary)" },
incident: { label: "Incident", icon: Fire, color: "var(--danger)" }, incident: { label: "Incident", icon: Fire, color: "var(--destructive)" },
} }
function highlightMatch(text: string, query: string): React.ReactNode { 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) const parts = text.split(regex)
return parts.map((part, i) => return parts.map((part, i) =>
regex.test(part) 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 : part
) )
} }
@@ -31,11 +31,11 @@ function highlightMatch(text: string, query: string): React.ReactNode {
function SkeletonRow() { function SkeletonRow() {
return ( return (
<div className="flex gap-3 px-3 py-3" style={{ height: 72 }}> <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="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-32 animate-pulse rounded bg-muted" />
<div className="h-3 w-48 animate-pulse rounded bg-[color:var(--panel-soft)]" /> <div className="h-3 w-48 animate-pulse rounded bg-muted" />
<div className="h-3 w-64 animate-pulse rounded bg-[color:var(--panel-soft)]" /> <div className="h-3 w-64 animate-pulse rounded bg-muted" />
</div> </div>
</div> </div>
) )
@@ -97,7 +97,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
return ( return (
<motion.div className="flex h-full flex-col" {...animationProps}> <motion.div className="flex h-full flex-col" {...animationProps}>
<div className="shrink-0 px-3 py-2"> <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" : ""} {results.length} result{results.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
@@ -144,7 +144,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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"> <div className="flex items-start gap-2.5">
<AgentAvatar agent={agent} size="sm" /> <AgentAvatar agent={agent} size="sm" />
@@ -152,7 +152,7 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{/* Row 1: Agent name, channel badge, timestamp */} {/* Row 1: Agent name, channel badge, timestamp */}
<div className="flex items-center gap-2"> <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"} {agent?.name ?? "Unknown Agent"}
</span> </span>
<span <span
@@ -162,18 +162,18 @@ export function SearchResults({ results, query, loading, agents, onNavigateToRes
<ChannelIcon size={10} weight="bold" /> <ChannelIcon size={10} weight="bold" />
{config.label} {config.label}
</span> </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)} {formatMessageTime(message.createdAt)}
</span> </span>
</div> </div>
{/* Row 2: Subject */} {/* 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)} {highlightMatch(message.subject, query)}
</div> </div>
{/* Row 3: Body snippet */} {/* 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)} {highlightMatch(message.body, query)}
</div> </div>
</div> </div>
@@ -81,16 +81,16 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
<ListRow key={document.id}> <ListRow key={document.id}>
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(document.id)}> <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)]"> <div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<FileText size={15} className="text-[color:var(--accent)]" /> <FileText size={15} className="text-primary" />
<span className="truncate">{document.title}</span> <span className="truncate">{document.title}</span>
</div> </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> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusPill status={document.status} /> <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="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-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(document.id)}><Trash 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>
</div> </div>
</ListRow> </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"> <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(); }} /> <DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === "__new__" ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
</Modal> </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> </div>
); );
} }
@@ -69,16 +69,16 @@ export function KnowledgeBaseManager({ companyId, entries, onRefresh }: {
<ListRow key={entry.id}> <ListRow key={entry.id}>
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(entry.id)}> <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)]"> <div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<Lightbulb size={15} className="text-[color:var(--accent)]" /> <Lightbulb size={15} className="text-primary" />
<span className="truncate">{entry.topic}</span> <span className="truncate">{entry.topic}</span>
</div> </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> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusPill status={entry.importance} /> <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="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-[color:var(--muted)] hover:text-[color:var(--danger)]" onClick={() => setDeleteId(entry.id)}><Trash 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>
</div> </div>
</ListRow> </ListRow>
@@ -83,16 +83,16 @@ export function MeetingsManager({ companyId, meetings, agents, onRefresh }: {
<ListRow key={meeting.id}> <ListRow key={meeting.id}>
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(meeting.id)}> <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)]"> <div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<CalendarBlank size={15} className="text-[color:var(--accent)]" /> <CalendarBlank size={15} className="text-primary" />
<span className="truncate">{meeting.title}</span> <span className="truncate">{meeting.title}</span>
</div> </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> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusPill status={meeting.status} /> <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-muted-foreground hover:text-foreground" 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-destructive" onClick={() => setDeleteId(meeting.id)}><Trash size={14} /></button>
</div> </div>
</div> </div>
</ListRow> </ListRow>
@@ -13,8 +13,8 @@ export function SectionHeader({
return ( return (
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div> <div>
<div className="text-[16px] font-semibold text-[color:var(--text)]">{title}</div> <div className="text-[16px] font-semibold text-foreground">{title}</div>
<div className="text-[12px] text-[color:var(--muted)]">{count} item{count === 1 ? "" : "s"}</div> <div className="text-[12px] text-muted-foreground">{count} item{count === 1 ? "" : "s"}</div>
</div> </div>
<ActionButton label="New" icon={Plus} tone="accent" onClick={action} /> <ActionButton label="New" icon={Plus} tone="accent" onClick={action} />
</div> </div>
@@ -75,16 +75,16 @@ export function SprintManager({ companyId, sprints, onRefresh }: {
<ListRow key={sprint.id}> <ListRow key={sprint.id}>
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setEditingId(sprint.id)}> <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)]"> <div className="flex items-center gap-2 text-[13px] font-medium text-foreground">
<Rows size={15} className="text-[color:var(--accent)]" /> <Rows size={15} className="text-primary" />
<span className="truncate">{sprint.name}</span> <span className="truncate">{sprint.name}</span>
</div> </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> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<StatusPill status={sprint.status} /> <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-muted-foreground hover:text-foreground" 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-destructive" onClick={() => setDeleteId(sprint.id)}><Trash size={14} /></button>
</div> </div>
</div> </div>
</ListRow> </ListRow>
+14 -14
View File
@@ -137,7 +137,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
<div className="grid gap-4"> <div className="grid gap-4">
{submitError ? <InlineNotice message={submitError} /> : null} {submitError ? <InlineNotice message={submitError} /> : null}
{needsLogin ? ( {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="text-[11px] font-semibold uppercase tracking-[0.08em]">{authActionCopy.title}</div>
<div className="mt-1.5 leading-relaxed">{authActionCopy.message}</div> <div className="mt-1.5 leading-relaxed">{authActionCopy.message}</div>
<div className="mt-3"> <div className="mt-3">
@@ -163,17 +163,17 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null} ) : null}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <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"> <div className="flex items-center gap-3">
<StatusPill status={connector.status} /> <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>
<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"> <div className="grid grid-cols-2 gap-2">
{Object.entries(connector.capabilityMatrix).map(([key, value]) => ( {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} {key}
</div> </div>
))} ))}
@@ -182,7 +182,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
</div> </div>
<div> <div>
<Input label="Command" value={connector.command} onChange={() => undefined} readOnly /> <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. AgentCompany pins connector execution to the audited binary for this runtime. Command overrides are not accepted from the renderer.
</div> </div>
</div> </div>
@@ -195,13 +195,13 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
options={modelSelectOptions} options={modelSelectOptions}
/> />
{loadingModelOptions ? ( {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} ) : null}
{modelOptionsError ? ( {modelOptionsError ? (
<InlineNotice message={modelOptionsError} /> <InlineNotice message={modelOptionsError} />
) : null} ) : null}
{requiresExplicitModel && !model.trim() ? ( {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. OpenCode is detected, but it still needs an explicit provider/model selection before AgentCompany can mark it ready.
</div> </div>
) : null} ) : 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="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} /> <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. Connector health checks run automatically on app launch, after save, and after terminal login is opened.
{autoChecking ? " Refreshing connector state..." : ""} {autoChecking ? " Refreshing connector state..." : ""}
</div> </div>
{connector.lastError ? ( {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="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-[color:var(--muted)]">Latest health-check detail</div> <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>{connector.lastError}</div>
</div> </div>
) : null} ) : null}
@@ -249,9 +249,9 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
disabled={requiresExplicitModel && !model.trim()} disabled={requiresExplicitModel && !model.trim()}
/> />
</div> </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="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-[color:var(--muted)]">Configuration doc</div> <div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Configuration doc</div>
<pre className="mono whitespace-pre-wrap">{connector.configurationDoc}</pre> <pre className="font-mono whitespace-pre-wrap">{connector.configurationDoc}</pre>
</div> </div>
</div> </div>
); );
@@ -32,7 +32,7 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
if (recentActivity.length === 0) { if (recentActivity.length === 0) {
return ( return (
<div className="flex h-full items-center justify-center py-8 text-center"> <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> </div>
); );
} }
@@ -49,23 +49,23 @@ export function ActivityFeed({ activity, agents, limit = 15 }: { activity: Activ
key={item.id} key={item.id}
initial={index === 0 ? { opacity: 0, y: -8 } : false} initial={index === 0 ? { opacity: 0, y: -8 } : false}
animate={{ opacity: 1, y: 0 }} 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"> <div className="mt-0.5 shrink-0">
<Icon size={14} className="text-[color:var(--muted)]" /> <Icon size={14} className="text-muted-foreground" />
</div> </div>
<div className="min-w-0 flex-1"> <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="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 ? ( {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} ) : null}
</div> </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> </div>
</motion.div> </motion.div>
); );
@@ -78,9 +78,9 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
<div className="space-y-2"> <div className="space-y-2">
{incidents.map((incident) => { {incidents.map((incident) => {
const isHardStop = incident.thresholdType === "hard_stop"; const isHardStop = incident.thresholdType === "hard_stop";
const borderColor = isHardStop ? "border-[color:var(--danger)]" : "border-[color:var(--warn)]"; const borderColor = isHardStop ? "border-[color:var(--destructive)]" : "border-[color:var(--destructive)]";
const bgColor = isHardStop ? "bg-[color:var(--danger-soft)]" : "bg-[color:var(--warn-soft)]"; const bgColor = isHardStop ? "bg-red-100 dark:bg-red-900/50" : "bg-yellow-100 dark:bg-yellow-900/50";
const textColor = isHardStop ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"; 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; const pct = incident.amountLimit > 0 ? Math.round((incident.amountObserved / incident.amountLimit) * 100) : 0;
return ( return (
@@ -88,7 +88,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
key={incident.id} key={incident.id}
role="alert" role="alert"
aria-live="assertive" 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="flex items-start gap-3">
<div className="mt-0.5 shrink-0"> <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}`}> <div className={`text-[13px] font-semibold ${textColor}`}>
{isHardStop ? "Budget Hard Stop" : "Budget Warning"} {isHardStop ? "Budget Hard Stop" : "Budget Warning"}
</div> </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}%) {getScopeName(incident.scopeType, incident.scopeId)} — ${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)} ({pct}%)
</div> </div>
<button <button
type="button" type="button"
onClick={handleViewCosts} 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 View Costs
</button> </button>
@@ -119,7 +119,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
onClick={handleDismiss} onClick={handleDismiss}
title="Dismiss budget alerts" title="Dismiss budget alerts"
aria-label="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} /> <X size={14} />
</button> </button>
@@ -132,17 +132,17 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
<div <div
role="alert" role="alert"
aria-live="assertive" 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="flex items-start gap-3">
<div className="mt-0.5 shrink-0"> <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>
<div className="min-w-0 flex-1"> <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 Budget Alert
</div> </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 {agentsOverThreshold.length} agent(s) approaching budget limit
</div> </div>
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1"> <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} key={agent.id}
className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${ className={`whitespace-nowrap text-[11px] font-[family-name:var(--font-mono)] ${
agent.pct >= 100 agent.pct >= 100
? "text-[color:var(--danger)]" ? "text-destructive"
: "text-[color:var(--warn)]" : "text-yellow-600 dark:text-yellow-400"
}`} }`}
style={{ fontVariantNumeric: "tabular-nums" }} style={{ fontVariantNumeric: "tabular-nums" }}
> >
@@ -163,7 +163,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
<button <button
type="button" type="button"
onClick={handleViewCosts} 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 View Costs
</button> </button>
@@ -173,7 +173,7 @@ export function BudgetAlertBanner({ agents, onNavigate, companyId }: BudgetAlert
onClick={handleDismiss} onClick={handleDismiss}
title="Dismiss budget alert" title="Dismiss budget alert"
aria-label="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} /> <X size={14} />
</button> </button>
@@ -37,7 +37,7 @@ export function ChartSection({ runs, costs, agents, companyId }: ChartSectionPro
const content = ( const content = (
<div className="space-y-6"> <div className="space-y-6">
{/* Section header */} {/* 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 Performance
</div> </div>
@@ -74,7 +74,7 @@ export function ChartSection({ runs, costs, agents, companyId }: ChartSectionPro
</div> </div>
{/* Budget Utilization section */} {/* 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 Budget Utilization
</div> </div>
<BudgetUtilization companyId={companyId} agents={agents} /> <BudgetUtilization companyId={companyId} agents={agents} />
+37 -37
View File
@@ -78,18 +78,18 @@ export function Dashboard(props: DashboardProps) {
{/* Company status line */} {/* Company status line */}
{companyName ? ( {companyName ? (
<div> <div>
<h2 className="text-[28px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{companyName}</h2> <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-[color:var(--muted)]"> <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="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 {activeAgents.length} {activeAgents.length === 1 ? "agent" : "agents"} active
</span> </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="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 {activeTasks.length} {activeTasks.length === 1 ? "task" : "tasks"} in progress
</span> </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> <span>{tasks.filter((tk) => tk.status === "done").length} completed</span>
</div> </div>
</div> </div>
@@ -139,8 +139,8 @@ export function Dashboard(props: DashboardProps) {
{/* Needs Attention — moved to top, only shows when items exist */} {/* Needs Attention — moved to top, only shows when items exist */}
{hasAttentionItems ? ( {hasAttentionItems ? (
<section> <section>
<div className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[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-[color:var(--warn)]" /> <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-yellow-600 dark:bg-yellow-400" />
{t("dash.needsAttentionSection")} {t("dash.needsAttentionSection")}
</div> </div>
<div className="space-y-1.5"> <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] }} transition={{ duration: 0.22, delay: i * 0.04, ease: [0.16, 1, 0.3, 1] }}
type="button" type="button"
onClick={() => onNavigate("approvals", approval.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div> <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-[color:var(--muted)]"> <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> <span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null} {requester ? <span>by {requester.name}</span> : null}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</motion.button> </motion.button>
); );
})} })}
@@ -175,16 +175,16 @@ export function Dashboard(props: DashboardProps) {
key={run.id} key={run.id}
type="button" type="button"
onClick={() => onNavigate("runs", run.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div> <div className="truncate text-[13px] font-medium text-foreground">{run.summary || "Run failed"}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]"> <div className="mt-0.5 text-[11px] text-muted-foreground">
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"} {agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
); );
})} })}
@@ -195,16 +195,16 @@ export function Dashboard(props: DashboardProps) {
key={msg.id} key={msg.id}
type="button" type="button"
onClick={() => onNavigate("communication", msg.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{msg.subject}</div> <div className="truncate text-[13px] font-medium text-foreground">{msg.subject}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]"> <div className="mt-0.5 text-[11px] text-muted-foreground">
{sender?.name ?? "Unknown"} — {msg.channel}{msg.priority === "urgent" ? " · urgent" : ""} {sender?.name ?? "Unknown"} — {msg.channel}{msg.priority === "urgent" ? " · urgent" : ""}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
); );
})} })}
@@ -215,36 +215,36 @@ export function Dashboard(props: DashboardProps) {
{/* Active Work — what's running right now */} {/* Active Work — what's running right now */}
<section> <section>
<div className="mb-3 flex items-center justify-between"> <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 Active Work
</div> </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} /> All runs <ArrowRight size={12} />
</button> </button>
</div> </div>
{activeWork.length > 0 ? ( {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 }) => ( {activeWork.map(({ run, agent, task }) => (
<button <button
key={run.id} key={run.id}
type="button" type="button"
onClick={() => onNavigate("runs", run.id)} 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"> <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" ? ( {run.status === "running" ? (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2"> <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="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-[color:var(--success)]" /> <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-600 dark:bg-green-400" />
</span> </span>
) : null} ) : null}
</div> </div>
<div className="min-w-0 flex-1"> <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)}`} {task?.title ?? run.summary ?? `Run ${run.id.slice(0, 8)}`}
</div> </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} {agent?.name ?? "Unknown agent"} · {run.status}
</div> </div>
</div> </div>
@@ -252,9 +252,9 @@ export function Dashboard(props: DashboardProps) {
))} ))}
</div> </div>
) : ( ) : (
<div className="rounded-[8px] border border-dashed border-[color:var(--line)] py-8 text-center"> <div className="rounded-lg border border-dashed border-border py-8 text-center">
<div className="text-[13px] text-[color:var(--muted)]">{t("dash.allQuiet")}</div> <div className="text-[13px] text-muted-foreground">{t("dash.allQuiet")}</div>
<div className="mt-1 text-[11px] text-[color:var(--muted)]">No tasks currently running</div> <div className="mt-1 text-[11px] text-muted-foreground">No tasks currently running</div>
</div> </div>
)} )}
</section> </section>
@@ -262,11 +262,11 @@ export function Dashboard(props: DashboardProps) {
{/* Recent Activity */} {/* Recent Activity */}
<section> <section>
<div className="mb-3 flex items-center justify-between"> <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")} {t("dash.recentActivity")}
</div> </div>
</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} /> <ActivityFeed activity={activity} agents={agents} limit={10} />
</div> </div>
</section> </section>
@@ -15,7 +15,7 @@ import {
import type { AgentRecord, RunRecord, SectionId, TaskRecord } from "@shared/types"; import type { AgentRecord, RunRecord, SectionId, TaskRecord } from "@shared/types";
const DEPARTMENT_INFO: Record<string, { label: string; icon: React.ElementType; color: string }> = { 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" }, engineering: { label: "Engineering", icon: Cpu, color: "#6366f1" },
product: { label: "Product", icon: Rocket, color: "#8b5cf6" }, product: { label: "Product", icon: Rocket, color: "#8b5cf6" },
design: { label: "Design", icon: Target, color: "#ec4899" }, design: { label: "Design", icon: Target, color: "#ec4899" },
@@ -76,7 +76,7 @@ export function DepartmentOverview({
return ( return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
{departments.map(([deptId, data]) => { {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 Icon = info.icon;
const activeTasks = data.tasks.filter((t) => t.status === "in_progress" || t.status === "in_review").length; 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; const doneTasks = data.tasks.filter((t) => t.status === "done").length;
@@ -88,33 +88,33 @@ export function DepartmentOverview({
key={deptId} key={deptId}
type="button" type="button"
onClick={() => onNavigate("agents")} 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="absolute left-0 top-0 h-full w-[3px]" style={{ backgroundColor: info.color }} />
<div className="flex items-center gap-2 pl-1"> <div className="flex items-center gap-2 pl-1">
<Icon size={15} weight="fill" style={{ color: info.color }} /> <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 && ( {runningAgents > 0 && (
<span className="ml-auto flex items-center gap-1"> <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="h-1.5 w-1.5 animate-pulse rounded-full bg-green-600 dark:bg-green-400" />
<span className="text-[9px] text-[color:var(--success)]">{runningAgents}</span> <span className="text-[9px] text-green-600 dark:text-green-400">{runningAgents}</span>
</span> </span>
)} )}
</div> </div>
<div className="mt-2 grid grid-cols-3 gap-1 pl-1"> <div className="mt-2 grid grid-cols-3 gap-1 pl-1">
<div> <div>
<div className="text-[16px] font-bold tabular-nums text-[color:var(--text)]">{data.agents.length}</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-[color:var(--muted)]">People</div> <div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">People</div>
</div> </div>
<div> <div>
<div className="text-[16px] font-bold tabular-nums text-[color:var(--accent)]">{activeTasks}</div> <div className="text-[16px] font-bold tabular-nums text-primary">{activeTasks}</div>
<div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Active</div> <div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Active</div>
</div> </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} {blockedTasks > 0 ? blockedTasks : doneTasks}
</div> </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"} {blockedTasks > 0 ? "Blocked" : "Done"}
</div> </div>
</div> </div>
@@ -20,22 +20,22 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
key={goal.id} key={goal.id}
type="button" type="button"
onClick={() => onNavigate("goals")} 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 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<Target size={15} weight="fill" className="shrink-0 text-[color:var(--accent)]" /> <Target size={15} weight="fill" className="shrink-0 text-primary" />
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span> <span className="truncate text-[13px] font-medium text-foreground">{goal.title}</span>
</div> </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>
<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 <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}%` }} style={{ width: `${pct}%` }}
/> />
</div> </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> </button>
); );
})} })}
@@ -58,11 +58,11 @@ export function computeHealthScore(props: HealthInputProps): number {
} }
function healthLabel(score: number): { label: string; tone: string } { function healthLabel(score: number): { label: string; tone: string } {
if (score >= 90) return { label: "Excellent", tone: "text-[color:var(--success)]" }; if (score >= 90) return { label: "Excellent", tone: "text-green-600 dark:text-green-400" };
if (score >= 70) return { label: "Good", tone: "text-[color:var(--success)]" }; if (score >= 70) return { label: "Good", tone: "text-green-600 dark:text-green-400" };
if (score >= 50) return { label: "Fair", tone: "text-[color:var(--warn)]" }; if (score >= 50) return { label: "Fair", tone: "text-yellow-600 dark:text-yellow-400" };
if (score >= 30) return { label: "Poor", tone: "text-[color:var(--warn)]" }; if (score >= 30) return { label: "Poor", tone: "text-yellow-600 dark:text-yellow-400" };
return { label: "Critical", tone: "text-[color:var(--danger)]" }; return { label: "Critical", tone: "text-destructive" };
} }
export function HealthRing({ score }: { score: number }) { export function HealthRing({ score }: { score: number }) {
@@ -72,16 +72,16 @@ export function HealthRing({ score }: { score: number }) {
const { label, tone } = healthLabel(score); const { label, tone } = healthLabel(score);
const strokeColor = score >= 70 const strokeColor = score >= 70
? "var(--success)" ? "var(--primary)"
: score >= 50 : score >= 50
? "var(--warn)" ? "var(--destructive)"
: "var(--danger)"; : "var(--destructive)";
return ( return (
<div className="flex items-center gap-5"> <div className="flex items-center gap-5">
<div className="relative h-[90px] w-[90px] shrink-0"> <div className="relative h-[90px] w-[90px] shrink-0">
<svg viewBox="0 0 88 88" className="h-full w-full -rotate-90"> <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 <motion.circle
cx="44" cx="44"
cy="44" cy="44"
@@ -101,7 +101,7 @@ export function HealthRing({ score }: { score: number }) {
</div> </div>
</div> </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 className={`text-[16px] font-semibold ${tone}`}>{label}</div>
</div> </div>
</div> </div>
@@ -10,19 +10,19 @@ interface KanbanProps {
} }
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [ 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: "backlog", label: "Backlog", tone: "text-muted-foreground", dotColor: "bg-[color:var(--muted-foreground)]" },
{ status: "todo", label: "Todo", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" }, { 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-[color:var(--accent)]", dotColor: "bg-[color:var(--accent)]" }, { status: "in_progress", label: "In Progress", tone: "text-primary", dotColor: "bg-primary" },
{ status: "in_review", label: "In Review", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" }, { 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-[color:var(--success)]", dotColor: "bg-[color:var(--success)]" }, { 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-[color:var(--danger)]", dotColor: "bg-[color:var(--danger)]" }, { status: "blocked", label: "Blocked", tone: "text-destructive", dotColor: "bg-destructive" },
]; ];
const priorityColors: Record<string, string> = { const priorityColors: Record<string, string> = {
critical: "border-l-[color:var(--danger)]", critical: "border-l-[color:var(--destructive)]",
high: "border-l-[color:var(--warn)]", high: "border-l-[color:var(--destructive)]",
medium: "border-l-[color:var(--accent)]", medium: "border-l-[color:var(--primary)]",
low: "border-l-[color:var(--muted)]", low: "border-l-[color:var(--muted-foreground)]",
}; };
export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) { export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) {
@@ -44,10 +44,10 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
if (tasks.length === 0) { if (tasks.length === 0) {
return ( return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center"> <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-[color:var(--muted)]" /> <Briefcase size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div> <div className="text-[14px] font-medium text-muted-foreground">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="mt-1 text-[12px] text-muted-foreground">Tasks created by agents will appear here on the board</div>
</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"> <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={`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={`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} {colTasks.length}
</span> </span>
</div> </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) => { {colTasks.map((task) => {
const assignee = agents.find((a) => a.id === task.assigneeAgentId); const assignee = agents.find((a) => a.id === task.assigneeAgentId);
const activeRun = runs.find((r) => r.id === task.activeRunId); const activeRun = runs.find((r) => r.id === task.activeRunId);
@@ -75,27 +75,27 @@ export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps)
key={task.id} key={task.id}
type="button" type="button"
onClick={() => onSelectTask(task.id)} 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"> <div className="mt-1.5 flex items-center gap-2">
{assignee ? ( {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} /> <Robot size={10} />
{assignee.name} {assignee.name}
</span> </span>
) : ( ) : (
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span> <span className="text-[10px] italic text-muted-foreground">Unassigned</span>
)} )}
{isRunning ? ( {isRunning ? (
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-[color:var(--success)]"> <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-[color:var(--success)]" aria-hidden="true" /> <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-green-600 dark:bg-green-400" aria-hidden="true" />
Running Running
</span> </span>
) : null} ) : null}
</div> </div>
{task.priority === "critical" || task.priority === "high" ? ( {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} {task.priority}
</div> </div>
) : null} ) : null}
@@ -16,41 +16,41 @@ export function MetricCard({
subtitle?: string; subtitle?: string;
}) { }) {
const toneMap = { const toneMap = {
default: "text-[color:var(--text)]", default: "text-foreground",
success: "text-[color:var(--success)]", success: "text-green-600 dark:text-green-400",
warn: "text-[color:var(--warn)]", warn: "text-yellow-600 dark:text-yellow-400",
danger: "text-[color:var(--danger)]", danger: "text-destructive",
accent: "text-[color:var(--accent)]", accent: "text-primary",
}; };
const bgMap = { const bgMap = {
default: "bg-[color:var(--panel-soft)]", default: "bg-muted",
success: "bg-[color:var(--success-soft)]", success: "bg-green-100 dark:bg-green-900/50",
warn: "bg-[color:var(--warn-soft)]", warn: "bg-yellow-100 dark:bg-yellow-900/50",
danger: "bg-[color:var(--danger-soft)]", danger: "bg-red-100 dark:bg-red-900/50",
accent: "bg-[color:var(--accent-soft)]", accent: "bg-accent",
}; };
const borderMap = { const borderMap = {
default: "border-l-transparent", default: "border-l-transparent",
success: "border-l-[color:var(--success)]", success: "border-l-[color:var(--primary)]",
warn: "border-l-[color:var(--warn)]", warn: "border-l-[color:var(--destructive)]",
danger: "border-l-[color:var(--danger)]", danger: "border-l-[color:var(--destructive)]",
accent: "border-l-[color:var(--accent)]", accent: "border-l-[color:var(--primary)]",
}; };
return ( return (
<button <button
type="button" type="button"
onClick={onClick} 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]} /> <Icon size={24} weight="fill" className={toneMap[tone]} />
</div> </div>
<div className="min-w-0 flex-1"> <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> <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> </div>
</button> </button>
); );
@@ -48,7 +48,7 @@ export function NeedsAttention({
return ( return (
<section> <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"> <div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => { {pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId); const requester = agents.find((a) => a.id === approval.requestedByAgentId);
@@ -57,17 +57,17 @@ export function NeedsAttention({
key={approval.id} key={approval.id}
type="button" type="button"
onClick={() => onNavigate("approvals", approval.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div> <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-[color:var(--muted)]"> <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> <span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null} {requester ? <span>by {requester.name}</span> : null}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
); );
})} })}
@@ -78,16 +78,16 @@ export function NeedsAttention({
key={run.id} key={run.id}
type="button" type="button"
onClick={() => onNavigate("runs", run.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div> <div className="truncate text-[13px] font-medium text-foreground">{run.summary || "Run failed"}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]"> <div className="mt-0.5 text-[11px] text-muted-foreground">
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"} {agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
); );
})} })}
@@ -98,16 +98,16 @@ export function NeedsAttention({
key={message.id} key={message.id}
type="button" type="button"
onClick={() => onNavigate("communication", message.id)} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{message.subject}</div> <div className="truncate text-[13px] font-medium text-foreground">{message.subject}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]"> <div className="mt-0.5 text-[11px] text-muted-foreground">
{sender?.name ?? "Unknown"} — {message.channel} {message.priority === "urgent" ? "· urgent" : ""} {sender?.name ?? "Unknown"} — {message.channel} {message.priority === "urgent" ? "· urgent" : ""}
</div> </div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
); );
})} })}
@@ -116,14 +116,14 @@ export function NeedsAttention({
key={action.id} key={action.id}
type="button" type="button"
onClick={() => onNavigate("social")} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{action.resultSummary || action.actionType}</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-[color:var(--muted)]">Browser action {action.status.replaceAll("_", " ")}</div> <div className="mt-0.5 whitespace-nowrap text-[11px] text-muted-foreground">Browser action {action.status.replaceAll("_", " ")}</div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
))} ))}
{stalledWorkflows.slice(0, 3).map((workflow) => ( {stalledWorkflows.slice(0, 3).map((workflow) => (
@@ -131,14 +131,14 @@ export function NeedsAttention({
key={workflow.id} key={workflow.id}
type="button" type="button"
onClick={() => onNavigate("automation")} 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="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{workflow.name}</div> <div className="truncate text-[13px] font-medium text-foreground">{workflow.name}</div>
<div className="mt-0.5 whitespace-nowrap text-[11px] text-[color:var(--muted)]">Workflow {workflow.status}</div> <div className="mt-0.5 whitespace-nowrap text-[11px] text-muted-foreground">Workflow {workflow.status}</div>
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
))} ))}
</div> </div>
@@ -3,12 +3,12 @@ import type { AgentRecord } from "@shared/types";
import { getAgentInitials } from "../../lib/formatters"; import { getAgentInitials } from "../../lib/formatters";
const statusGlow: Record<string, string> = { const statusGlow: Record<string, string> = {
running: "ring-2 ring-[color:var(--success)] ring-opacity-50", running: "ring-2 ring-[color:var(--primary)] ring-opacity-50",
active: "ring-1 ring-[color:var(--success)] ring-opacity-30", active: "ring-1 ring-[color:var(--primary)] ring-opacity-30",
idle: "", idle: "",
paused: "opacity-60", paused: "opacity-60",
error: "ring-2 ring-[color:var(--danger)] ring-opacity-50", error: "ring-2 ring-[color:var(--destructive)] ring-opacity-50",
pending_approval: "ring-1 ring-[color:var(--warn)] ring-opacity-40", pending_approval: "ring-1 ring-[color:var(--destructive)] ring-opacity-40",
terminated: "opacity-40", terminated: "opacity-40",
}; };
@@ -20,39 +20,39 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
<button <button
type="button" type="button"
onClick={onClick} 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="relative">
<div className={`flex h-12 w-12 items-center justify-center rounded-full text-[15px] font-bold ${ <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} {initials}
</div> </div>
{agent.status === "running" ? ( {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 -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="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-[color:var(--success)]" /> <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-600 dark:bg-green-400" />
</span> </span>
) : ( ) : (
<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={{ style={{
backgroundColor: agent.status === "error" backgroundColor: agent.status === "error"
? "var(--danger)" ? "var(--destructive)"
: agent.status === "active" : agent.status === "active"
? "var(--success)" ? "var(--primary)"
: agent.status === "pending_approval" : agent.status === "pending_approval"
? "var(--warn)" ? "var(--destructive)"
: agent.status === "paused" || agent.status === "terminated" : agent.status === "paused" || agent.status === "terminated"
? "var(--muted)" ? "var(--muted-foreground)"
: "var(--muted)", : "var(--muted-foreground)",
}} }}
/> />
)} )}
</div> </div>
<div className="w-full min-w-0"> <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-[12px] font-semibold text-foreground">{agent.name}</div>
<div className="truncate text-[10px] text-[color:var(--muted)]">{agent.title || agent.role}</div> <div className="truncate text-[10px] text-muted-foreground">{agent.title || agent.role}</div>
</div> </div>
</button> </button>
); );
@@ -61,9 +61,9 @@ function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void
export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) { export function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
if (agents.length === 0) { if (agents.length === 0) {
return ( return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-10 text-center"> <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-[color:var(--muted)]" /> <Robot size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div> <div className="text-[14px] font-medium text-muted-foreground">No agents hired yet</div>
</div> </div>
); );
} }
@@ -41,33 +41,33 @@ export function RecoveryBanner() {
const handleDismissAll = () => setDismissed(true); const handleDismissAll = () => setDismissed(true);
return ( 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 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" /> <Warning size={18} weight="bold" />
<span>{runs.length} interrupted task(s) detected from previous session</span> <span>{runs.length} interrupted task(s) detected from previous session</span>
</div> </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} /> <X size={16} />
</button> </button>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{runs.map(run => ( {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"> <div className="min-w-0 flex-1">
<span className="truncate text-[color:var(--text)]">{run.taskTitle}</span> <span className="truncate text-foreground">{run.taskTitle}</span>
<span className="whitespace-nowrap text-[color:var(--muted)] ml-2">({run.agentName})</span> <span className="whitespace-nowrap text-muted-foreground ml-2">({run.agentName})</span>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => handleRetry(run)} 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 <ArrowClockwise size={14} /> Retry
</button> </button>
<button <button
onClick={() => handleFail(run)} 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 <Trash size={14} /> Cancel
</button> </button>
@@ -30,39 +30,39 @@ export function SprintProgress({ sprint, tasks }: { sprint: SprintRecord; tasks:
}, [tasks]); }, [tasks]);
return ( 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 justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarCheck size={16} className="text-[color:var(--accent)]" /> <CalendarCheck size={16} className="text-primary" />
<span className="truncate text-[12px] font-semibold text-[color:var(--text)]">{sprint.name}</span> <span className="truncate text-[12px] font-semibold text-foreground">{sprint.name}</span>
<StatusPill status={sprint.status} /> <StatusPill status={sprint.status} />
</div> </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} /> <Clock size={12} />
{daysLeft} days left {daysLeft} days left
</div> </div>
</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 <motion.div
className="h-full rounded-full bg-[color:var(--accent)]" className="h-full rounded-full bg-primary"
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${progress}%` }} animate={{ width: `${progress}%` }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.8, ease: "easeOut" }}
/> />
</div> </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> <span style={{ fontVariantNumeric: "tabular-nums" }}>{sprint.completedPoints}/{sprint.velocityPoints} points ({progress}%)</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="whitespace-nowrap text-[color:var(--success)]">{sprintTasks.done} done</span> <span className="whitespace-nowrap text-green-600 dark:text-green-400">{sprintTasks.done} done</span>
<span className="whitespace-nowrap text-[color:var(--accent)]">{sprintTasks.active} active</span> <span className="whitespace-nowrap text-primary">{sprintTasks.active} active</span>
<span className="whitespace-nowrap">{sprintTasks.total} total</span> <span className="whitespace-nowrap">{sprintTasks.total} total</span>
</div> </div>
</div> </div>
{sprint.goal && ( {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> </div>
); );
@@ -9,12 +9,12 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
if (t.status in counts) counts[t.status as keyof typeof counts]++; if (t.status in counts) counts[t.status as keyof typeof counts]++;
} }
return [ return [
{ label: "Backlog", count: counts.backlog, tone: "text-[color:var(--muted)]", bg: "bg-[color:var(--panel-soft)]" }, { label: "Backlog", count: counts.backlog, tone: "text-muted-foreground", bg: "bg-muted" },
{ label: "Todo", count: counts.todo, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" }, { 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-[color:var(--accent)]", bg: "bg-[color:var(--accent-soft)]" }, { label: "In Progress", count: counts.in_progress, tone: "text-primary", bg: "bg-accent" },
{ label: "In Review", count: counts.in_review, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" }, { 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-[color:var(--success)]", bg: "bg-[color:var(--success-soft)]" }, { 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-[color:var(--danger)]", bg: "bg-[color:var(--danger-soft)]" }, { label: "Blocked", count: counts.blocked, tone: "text-destructive", bg: "bg-red-100 dark:bg-red-900/50" },
]; ];
}, [tasks]); }, [tasks]);
@@ -22,12 +22,12 @@ export function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{stages.map((stage, i) => ( {stages.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-1.5"> <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-[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> </div>
{i < stages.length - 1 ? ( {i < stages.length - 1 ? (
<ArrowRight size={12} className="text-[color:var(--muted)]" /> <ArrowRight size={12} className="text-muted-foreground" />
) : null} ) : null}
</div> </div>
))} ))}
@@ -21,26 +21,26 @@ export function AgentWorkloadChart({ data, reducedMotion }: AgentWorkloadChartPr
<BarChart data={data}> <BarChart data={data}>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
stroke="var(--line)" stroke="var(--border)"
horizontal={true} horizontal={true}
vertical={false} vertical={false}
/> />
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={{ stroke: "var(--line)" }} axisLine={{ stroke: "var(--border)" }}
tickLine={false} tickLine={false}
/> />
<YAxis <YAxis
allowDecimals={false} allowDecimals={false}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
/> />
<Tooltip content={<ChartTooltip valueSuffix=" runs" />} /> <Tooltip content={<ChartTooltip valueSuffix=" runs" />} />
<Bar <Bar
dataKey="runs" dataKey="runs"
fill="var(--accent)" fill="var(--primary)"
fillOpacity={0.85} fillOpacity={0.85}
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
maxBarSize={40} maxBarSize={40}
@@ -16,9 +16,9 @@ interface BudgetRow {
} }
function getBarColor(pct: number): string { function getBarColor(pct: number): string {
if (pct >= 100) return "bg-[color:var(--danger)]"; if (pct >= 100) return "bg-destructive";
if (pct >= 80) return "bg-[color:var(--warn)]"; if (pct >= 80) return "bg-yellow-600 dark:bg-yellow-400";
return "bg-[color:var(--success)]"; return "bg-green-600 dark:bg-green-400";
} }
function scopeLabel(scopeType: string): string { function scopeLabel(scopeType: string): string {
@@ -77,12 +77,12 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
if (rows.length === 0) { if (rows.length === 0) {
return ( 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"> <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 No budget policies configured
</span> </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. Set budget policies in Settings to track utilization here.
</span> </span>
</div> </div>
@@ -91,19 +91,19 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
} }
return ( 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"> <div className="space-y-3">
{rows.map((row) => ( {rows.map((row) => (
<div key={row.id}> <div key={row.id}>
<div className="mb-1 flex items-center justify-between"> <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} {row.name}
<span className="text-[11px] font-normal text-[color:var(--muted)]"> <span className="text-[11px] font-normal text-muted-foreground">
{scopeLabel(row.scopeType)} {scopeLabel(row.scopeType)}
</span> </span>
</span> </span>
<span <span
className="text-[13px] text-[color:var(--muted)]" className="text-[13px] text-muted-foreground"
style={{ style={{
fontFamily: "var(--font-mono)", fontFamily: "var(--font-mono)",
fontVariantNumeric: "tabular-nums", fontVariantNumeric: "tabular-nums",
@@ -112,14 +112,14 @@ export function BudgetUtilization({ companyId, agents }: BudgetUtilizationProps)
{row.pct}% {row.pct}%
</span> </span>
</div> </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 <div
className={`h-1.5 rounded-full transition-all duration-300 ${getBarColor(row.pct)}`} className={`h-1.5 rounded-full transition-all duration-300 ${getBarColor(row.pct)}`}
style={{ width: `${Math.min(row.pct, 100)}%` }} style={{ width: `${Math.min(row.pct, 100)}%` }}
/> />
{/* 80% warning marker */} {/* 80% warning marker */}
<div <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%" }} style={{ left: "80%" }}
/> />
</div> </div>
@@ -21,15 +21,15 @@ export function ChartCard({
}: ChartCardProps) { }: ChartCardProps) {
return ( return (
<div <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" role="img"
aria-label={ariaLabel} aria-label={ariaLabel}
> >
<div className="mb-3 flex items-center justify-between"> <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} {title}
</span> </span>
<span className="text-[11px] text-[color:var(--muted)]"> <span className="text-[11px] text-muted-foreground">
{period} {period}
</span> </span>
</div> </div>
@@ -37,12 +37,12 @@ export function ChartCard({
<div className="flex h-[220px] flex-col items-center justify-center gap-2"> <div className="flex h-[220px] flex-col items-center justify-center gap-2">
<ChartLine <ChartLine
size={32} 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} {emptyHeading}
</span> </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} {emptyBody}
</span> </span>
</div> </div>
@@ -27,10 +27,10 @@ export function ChartTooltip({
return ( return (
<div <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 }} 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) => ( {payload.map((entry, i) => (
<div key={i} className="mt-1 flex items-center gap-2"> <div key={i} className="mt-1 flex items-center gap-2">
<span <span
@@ -38,7 +38,7 @@ export function ChartTooltip({
style={{ backgroundColor: entry.color }} style={{ backgroundColor: entry.color }}
/> />
<span <span
className="text-[13px] font-semibold text-[color:var(--text)]" className="text-[13px] font-semibold text-foreground"
style={{ style={{
fontFamily: "var(--font-mono)", fontFamily: "var(--font-mono)",
fontVariantNumeric: "tabular-nums", fontVariantNumeric: "tabular-nums",
@@ -22,20 +22,20 @@ export function CostTrendChart({ data, reducedMotion }: CostTrendChartProps) {
<AreaChart data={data}> <AreaChart data={data}>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
stroke="var(--line)" stroke="var(--border)"
horizontal={true} horizontal={true}
vertical={false} vertical={false}
/> />
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={formatShortDate} tickFormatter={formatShortDate}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={{ stroke: "var(--line)" }} axisLine={{ stroke: "var(--border)" }}
tickLine={false} tickLine={false}
/> />
<YAxis <YAxis
tickFormatter={(val: number) => "$" + val.toFixed(0)} tickFormatter={(val: number) => "$" + val.toFixed(0)}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
/> />
@@ -43,12 +43,12 @@ export function CostTrendChart({ data, reducedMotion }: CostTrendChartProps) {
<Area <Area
type="monotone" type="monotone"
dataKey="total" dataKey="total"
stroke="var(--accent)" stroke="var(--primary)"
fill="var(--accent-soft)" fill="var(--primary)"
fillOpacity={0.15} fillOpacity={0.15}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 4, fill: "var(--accent)" }} activeDot={{ r: 4, fill: "var(--primary)" }}
isAnimationActive={!reducedMotion} isAnimationActive={!reducedMotion}
/> />
</AreaChart> </AreaChart>
@@ -22,20 +22,20 @@ export function RunActivityChart({ data, reducedMotion }: RunActivityChartProps)
<AreaChart data={data}> <AreaChart data={data}>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
stroke="var(--line)" stroke="var(--border)"
horizontal={true} horizontal={true}
vertical={false} vertical={false}
/> />
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={formatShortDate} tickFormatter={formatShortDate}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={{ stroke: "var(--line)" }} axisLine={{ stroke: "var(--border)" }}
tickLine={false} tickLine={false}
/> />
<YAxis <YAxis
allowDecimals={false} allowDecimals={false}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
/> />
@@ -43,12 +43,12 @@ export function RunActivityChart({ data, reducedMotion }: RunActivityChartProps)
<Area <Area
type="monotone" type="monotone"
dataKey="total" dataKey="total"
stroke="var(--accent)" stroke="var(--primary)"
fill="var(--accent-soft)" fill="var(--primary)"
fillOpacity={0.3} fillOpacity={0.3}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 4, fill: "var(--accent)" }} activeDot={{ r: 4, fill: "var(--primary)" }}
isAnimationActive={!reducedMotion} isAnimationActive={!reducedMotion}
/> />
</AreaChart> </AreaChart>
@@ -32,21 +32,21 @@ export function SuccessRateChart({ data, reducedMotion }: SuccessRateChartProps)
<LineChart data={rateData}> <LineChart data={rateData}>
<CartesianGrid <CartesianGrid
strokeDasharray="3 3" strokeDasharray="3 3"
stroke="var(--line)" stroke="var(--border)"
horizontal={true} horizontal={true}
vertical={false} vertical={false}
/> />
<XAxis <XAxis
dataKey="date" dataKey="date"
tickFormatter={formatShortDate} tickFormatter={formatShortDate}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={{ stroke: "var(--line)" }} axisLine={{ stroke: "var(--border)" }}
tickLine={false} tickLine={false}
/> />
<YAxis <YAxis
domain={[0, 100]} domain={[0, 100]}
tickFormatter={(val: number) => `${val}%`} tickFormatter={(val: number) => `${val}%`}
tick={{ fontSize: 11, fill: "var(--muted)" }} tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
/> />
@@ -54,10 +54,10 @@ export function SuccessRateChart({ data, reducedMotion }: SuccessRateChartProps)
<Line <Line
type="monotone" type="monotone"
dataKey="rate" dataKey="rate"
stroke="var(--success)" stroke="var(--primary)"
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 4, fill: "var(--success)" }} activeDot={{ r: 4, fill: "var(--primary)" }}
isAnimationActive={!reducedMotion} isAnimationActive={!reducedMotion}
/> />
</LineChart> </LineChart>
@@ -43,15 +43,15 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="relative flex-1"> <div className="relative flex-1">
<MagnifyingGlass size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[color:var(--muted)]" /> <MagnifyingGlass size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<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")} /> <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> </div>
<Select value={entityFilter} onChange={setEntityFilter} options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? t("exec.allTypes") : v }))} /> <Select value={entityFilter} onChange={setEntityFilter} options={ENTITY_TYPE_OPTIONS.map((v) => ({ value: v, label: v === "all" ? t("exec.allTypes") : v }))} />
</div> </div>
{filtered.length === 0 ? <EmptyState title={t("exec.noMatchingActivity")} /> : ( {filtered.length === 0 ? <EmptyState title={t("exec.noMatchingActivity")} /> : (
<div <div
ref={parentRef} 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" role="listbox"
aria-label="Activity log" aria-label="Activity log"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -81,17 +81,17 @@ export function ActivityList({ activity }: { activity: ActivityRecord[] }) {
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<EntityIcon size={14} weight="duotone" className="shrink-0 text-[color:var(--muted)]" /> <EntityIcon size={14} weight="duotone" className="shrink-0 text-muted-foreground" />
<div className="truncate text-[13px] text-[color:var(--text)]">{entry.action}</div> <div className="truncate text-[13px] text-foreground">{entry.action}</div>
</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>
<div className="flex items-center gap-2 text-[13px] text-[color:var(--muted)]"> <div className="flex items-center gap-2 text-[13px] text-muted-foreground">
<span className="font-medium text-[color:var(--muted-strong)]">{entry.actor}</span> <span className="font-medium text-muted-foreground">{entry.actor}</span>
<span>·</span> <span>·</span>
<span className="truncate">{entry.detail || `${entry.entityType}:${entry.entityId}`}</span> <span className="truncate">{entry.detail || `${entry.entityType}:${entry.entityId}`}</span>
</div> </div>
@@ -11,12 +11,12 @@ import { SocialDraftPreview } from "./SocialDraftPreview"
import { useAppStore } from "../../state/store" import { useAppStore } from "../../state/store"
function getTypeBadgeClass(type: string): string { function getTypeBadgeClass(type: string): string {
if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" if (type === "hire_agent") return "bg-accent text-primary"
if (type === "approve_ceo_strategy") return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]" 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-[color:var(--danger-soft)] text-[color:var(--danger)]" if (type === "secret_access") return "bg-red-100 dark:bg-red-900/50 text-destructive"
if (type === "social_post") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" if (type === "social_post") return "bg-accent text-primary"
if (type === "deliverable_review") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" if (type === "deliverable_review") return "bg-accent text-primary"
return "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" return "bg-muted text-muted-foreground"
} }
export function ApprovalInspector({ approval, agents, tasks, documents, companyId, onDecide, onDeleted }: { 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"> <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) }} /> <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} {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"> <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> <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} /> <StatusPill status={approval.state} />
</div> </div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div> <div className="text-[13px] font-medium text-foreground">{approval.payloadSummary}</div>
{approval.impactSummary ? <div className="text-[13px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null} {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="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> <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-[color:var(--muted)]"><Briefcase size={14} />Task: <span className="font-medium text-[color:var(--text)]">{relatedTask.title}</span></div> : null} {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-[color:var(--muted)]">Created: <span className="mono text-[color:var(--text)]">{formatTime(approval.createdAt)}</span></div> <div className="text-muted-foreground">Created: <span className="font-mono text-foreground">{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} {approval.updatedAt !== approval.createdAt ? <div className="text-muted-foreground">Updated: <span className="font-mono text-foreground">{formatTime(approval.updatedAt)}</span></div> : null}
</div> </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 ( {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 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-[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-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-[color:var(--muted)]">Role:</span> <span className="text-[color:var(--text)]">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</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-[color:var(--muted)]">Title:</span> <span className="text-[color:var(--text)]">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null} {(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-[color:var(--muted)]">Capabilities:</span> <span className="text-[color:var(--text)]">{proposedAgent?.capabilities || reqSnap?.capabilities 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-[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-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-[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> <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-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{mgr.name}</span></div> : null} {mgr ? <div><span className="text-muted-foreground">Reports to:</span> <span className="text-foreground">{mgr.name}</span></div> : null}
</div></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} {approval.type === "deliverable_review" ? <DeliverableReviewPanel approval={approval} documents={documents} agents={agents} /> : null}
{isSocialPost && approval.payloadJson ? ( {isSocialPost && approval.payloadJson ? (
<SocialDraftPreview <SocialDraftPreview
@@ -133,37 +133,37 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
role="radio" role="radio"
aria-checked={scheduleMode === "immediate"} aria-checked={scheduleMode === "immediate"}
tabIndex={0} 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) }} onClick={() => { setScheduleMode("immediate"); setScheduleError(null) }}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { 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>
<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>
<label className="flex cursor-pointer items-center gap-2"> <label className="flex cursor-pointer items-center gap-2">
<span <span
role="radio" role="radio"
aria-checked={scheduleMode === "scheduled"} aria-checked={scheduleMode === "scheduled"}
tabIndex={0} 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")} onClick={() => setScheduleMode("scheduled")}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") 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>
<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> </label>
</div> </div>
{scheduleMode === "scheduled" ? ( {scheduleMode === "scheduled" ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock size={16} className="text-[color:var(--muted)]" /> <Clock size={16} className="text-muted-foreground" />
<input <input
type="datetime-local" type="datetime-local"
value={scheduledAt} value={scheduledAt}
min={minDatetime} min={minDatetime}
onChange={(e) => { setScheduledAt(e.target.value); setScheduleError(null) }} 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> </div>
) : null} ) : null}
@@ -171,7 +171,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
</div> </div>
) : null} ) : 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"> <div className="flex flex-wrap gap-3">
{!isDecided ? <> {!isDecided ? <>
<ActionButton <ActionButton
@@ -186,7 +186,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
</> : null} </> : null}
{isDecided ? <ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" loading={acting === "delete"} /> : null} {isDecided ? <ActionButton label="Delete" onClick={() => setConfirmDelete(true)} icon={Trash} tone="danger" loading={acting === "delete"} /> : null}
</div> </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> </div>
) )
} }
@@ -200,36 +200,36 @@ function DeliverableReviewPanel({ approval, documents, agents }: { approval: App
return ( return (
<div className="space-y-3"> <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) => { {fullDocs.length > 0 ? fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return ( 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 <button
type="button" type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)} 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="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-[color:var(--text)]">{doc.title}</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-[color:var(--muted)]" /> : <CaretDown size={14} className="shrink-0 text-[color:var(--muted)]" />} {isExpanded ? <CaretUp size={14} className="shrink-0 text-muted-foreground" /> : <CaretDown size={14} className="shrink-0 text-muted-foreground" />}
</button> </button>
{isExpanded ? ( {isExpanded ? (
<div className="border-t border-[color:var(--line)] px-4 py-3"> <div className="border-t border-border 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} {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-[color:var(--text)]">{doc.content || "No content."}</div> <div className="whitespace-pre-wrap text-[12px] leading-relaxed text-foreground">{doc.content || "No content."}</div>
</div> </div>
) : null} ) : null}
</div> </div>
) )
}) : docRefs.length > 0 ? docRefs.map((doc) => ( }) : 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]"> <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-[color:var(--accent-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[color:var(--accent)]">{doc.type.replaceAll("_", " ")}</span> <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-[color:var(--text)]">{doc.title}</span> <span className="font-medium text-foreground">{doc.title}</span>
</div> </div>
)) : <div className="text-[12px] text-[color:var(--muted)]">{approval.payloadSummary}</div>} )) : <div className="text-[12px] text-muted-foreground">{approval.payloadSummary}</div>}
{approval.impactSummary ? <div className="text-[11px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null} {approval.impactSummary ? <div className="text-[11px] text-muted-foreground">{approval.impactSummary}</div> : null}
</div> </div>
) )
} }
@@ -21,7 +21,7 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
return ( return (
<div <div
ref={parentRef} ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]" className="h-full overflow-auto border-y border-border"
role="listbox" role="listbox"
aria-label="Pending approvals" aria-label="Pending approvals"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -50,16 +50,16 @@ export function ApprovalList({ approvals, selectedApprovalId, onSelect }: { appr
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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 <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)} onClick={() => onSelect(approval.id)}
tabIndex={-1} 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="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} /> <StatusPill status={approval.state} />
</div> </div>
</button> </button>
@@ -28,14 +28,14 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
return ( return (
<div className="space-y-4"> <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="flex items-end justify-between gap-4 py-3">
<div className="text-[12px] text-[color:var(--muted)]">Known spend</div> <div className="text-[12px] text-muted-foreground">Known spend</div>
<div className="mono text-[22px] tracking-[-0.03em] text-[color:var(--text)]">{formatMoney(knownSpend)}</div> <div className="font-mono text-[22px] tracking-[-0.03em] text-foreground">{formatMoney(knownSpend)}</div>
</div> </div>
<div className="flex items-end justify-between gap-4 border-t border-[color:var(--line)] py-3"> <div className="flex items-end justify-between gap-4 border-t border-border py-3">
<div className="text-[12px] text-[color:var(--muted)]">Unattributed entries</div> <div className="text-[12px] text-muted-foreground">Unattributed entries</div>
<div className="mono text-[18px] text-[color:var(--text)]">{String(unattributedCount)}</div> <div className="font-mono text-[18px] text-foreground">{String(unattributedCount)}</div>
</div> </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 pct = status.amount > 0 ? (status.spent / status.amount) * 100 : 0
const isWarning = pct > 80 && pct <= 100 const isWarning = pct > 80 && pct <= 100
const isDanger = pct > 100 const isDanger = pct > 100
const barColor = isDanger ? "bg-[color:var(--danger)]" : isWarning ? "bg-[color:var(--warn)]" : "bg-[color:var(--success)]" const barColor = isDanger ? "bg-destructive" : isWarning ? "bg-yellow-600 dark:bg-yellow-400" : "bg-green-600 dark:bg-green-400"
const nameColor = isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--text)]" const nameColor = isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-foreground"
const scopeLabel = status.scopeType === "company" const scopeLabel = status.scopeType === "company"
? "Company" ? "Company"
@@ -58,18 +58,18 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
<ListRow key={status.policyId}> <ListRow key={status.policyId}>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <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} {status.scopeType}
</span> </span>
<div className={`text-[14px] font-medium ${nameColor}`}>{scopeLabel}</div> <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>
<div className="flex items-center gap-2"> <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> <div className="font-mono text-[12px] text-foreground">{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> <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> </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> </ListRow>
) )
})} })}
@@ -79,22 +79,22 @@ export function BudgetSummary({ agents, costs, companyId }: BudgetSummaryProps)
{agents.map((agent) => { {agents.map((agent) => {
const pct = agent.budgetMonthlyUsd > 0 ? (agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100 : 0 const pct = agent.budgetMonthlyUsd > 0 ? (agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100 : 0
const isWarning = pct > 80 && pct <= 100; const isDanger = pct > 100 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 barColor = isDanger ? "bg-destructive" : isWarning ? "bg-yellow-600 dark:bg-yellow-400" : "bg-green-600 dark:bg-green-400"
const nameColor = isDanger ? "text-[color:var(--danger)]" : isWarning ? "text-[color:var(--warn)]" : "text-[color:var(--text)]" const nameColor = isDanger ? "text-destructive" : isWarning ? "text-yellow-600 dark:text-yellow-400" : "text-foreground"
return ( return (
<ListRow key={agent.id}> <ListRow key={agent.id}>
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={`text-[14px] font-medium ${nameColor}`}>{agent.name}</div> <div className={`text-[14px] font-medium ${nameColor}`}>{agent.name}</div>
{isDanger ? <Warning size={14} weight="fill" className="text-[color:var(--danger)]" /> : null} {isDanger ? <Warning size={14} weight="fill" className="text-destructive" /> : null}
{isWarning ? <Warning size={14} weight="fill" className="text-[color:var(--warn)]" /> : null} {isWarning ? <Warning size={14} weight="fill" className="text-yellow-600 dark:text-yellow-400" /> : null}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="mono text-[12px] text-[color:var(--text)]">{formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)}</div> <div className="font-mono text-[12px] text-foreground">{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> <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> </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> </ListRow>
) )
})} })}
@@ -36,7 +36,7 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
</div> </div>
<div <div
ref={parentRef} 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" role="listbox"
aria-label="Cost entries" aria-label="Cost entries"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -66,11 +66,11 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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="flex items-center justify-between">
<div className="text-[13px] font-semibold text-[color:var(--text)]">Total ({filtered.length} entries)</div> <div className="text-[13px] font-semibold text-foreground">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="font-mono tabular-nums text-[13px] font-bold tracking-[-0.02em] text-foreground">{formatMoney(total)}</div>
</div> </div>
</div> </div>
) )
@@ -91,16 +91,16 @@ export function CostList({ costs, agents, runs }: { costs: ProfileSnapshot["cost
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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="flex items-center justify-between">
<div className="min-w-0 flex-1"> <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="truncate text-[13px] text-foreground">{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="text-[11px] text-muted-foreground">{agents.find((a) => a.id === entry.agentId)?.name ?? "Unknown agent"} · {entry.connectorId}</div>
</div> </div>
<div className="shrink-0 text-right"> <div className="shrink-0 text-right">
<div className="mono tabular-nums text-[13px] text-[color:var(--text)]">{formatMoney(entry.amountUsd)}</div> <div className="font-mono tabular-nums text-[13px] text-foreground">{formatMoney(entry.amountUsd)}</div>
{entry.unattributed ? <div className="text-[11px] text-[color:var(--warn)]">unattributed</div> : null} {entry.unattributed ? <div className="text-[11px] text-yellow-600 dark:text-yellow-400">unattributed</div> : null}
</div> </div>
</div> </div>
</div> </div>
@@ -11,10 +11,10 @@ interface IncidentListProps {
} }
const STATUS_STYLES: Record<string, string> = { const STATUS_STYLES: Record<string, string> = {
open: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]", open: "text-destructive bg-red-100 dark:bg-red-900/50",
resolved: "text-[color:var(--success)] bg-[color:var(--success-soft)]", resolved: "text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/50",
auto_resolved_limit_increase: "text-[color:var(--accent)] bg-[color:var(--accent-soft)]", auto_resolved_limit_increase: "text-primary bg-accent",
auto_closed: "text-[color:var(--muted-strong)] bg-[color:var(--panel-soft)]", auto_closed: "text-muted-foreground bg-muted",
}; };
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
@@ -56,8 +56,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
return ( return (
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between px-4 py-3"> <div className="flex items-center justify-between px-4 py-3">
<h3 className="text-[15px] font-semibold text-[color:var(--text)]">Budget Incidents</h3> <h3 className="text-[15px] font-semibold text-foreground">Budget Incidents</h3>
<div className="flex items-center gap-1 rounded-[6px] bg-[color:var(--panel-soft)] p-0.5"> <div className="flex items-center gap-1 rounded-lg bg-muted p-0.5">
{(["open", "all"] as const).map((f) => ( {(["open", "all"] as const).map((f) => (
<button <button
key={f} key={f}
@@ -65,8 +65,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
onClick={() => setFilter(f)} onClick={() => setFilter(f)}
className={`rounded-[5px] px-3 py-1 text-[12px] font-medium transition ${ className={`rounded-[5px] px-3 py-1 text-[12px] font-medium transition ${
filter === f filter === f
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" ? "bg-card text-foreground shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:text-foreground"
}`} }`}
> >
{f === "open" ? "Open" : "All"} {f === "open" ? "Open" : "All"}
@@ -93,8 +93,8 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
<span <span
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${ className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] ${
isWarn isWarn
? "text-[color:var(--warn)] bg-[color:var(--warn-soft)]" ? "text-yellow-600 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/50"
: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]" : "text-destructive bg-red-100 dark:bg-red-900/50"
}`} }`}
> >
{isWarn ? <Warning size={10} weight="bold" /> : null} {isWarn ? <Warning size={10} weight="bold" /> : null}
@@ -105,13 +105,13 @@ export function IncidentList({ companyId, agents, projects }: IncidentListProps)
</span> </span>
</div> </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="capitalize">{incident.scopeType}</span>
<span className="text-[color:var(--muted)]">&middot;</span> <span className="text-muted-foreground">&middot;</span>
<span className="font-medium text-[color:var(--text)]">{getScopeName(incident.scopeType, incident.scopeId)}</span> <span className="font-medium text-foreground">{getScopeName(incident.scopeType, incident.scopeId)}</span>
</div> </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" }}> <span className="font-[family-name:var(--font-mono)]" style={{ fontVariantNumeric: "tabular-nums" }}>
${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)} ${(incident.amountObserved / 100).toFixed(2)} / ${(incident.amountLimit / 100).toFixed(2)}
</span> </span>
@@ -13,11 +13,11 @@ export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agen
const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null
const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null
return ( 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"> <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-[color:var(--accent)]" /> <Robot size={16} weight="fill" className="mt-0.5 shrink-0 text-primary" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{task.title}</div> <div className="truncate text-[13px] font-medium text-foreground">{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="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>
</div> </div>
) )
@@ -136,9 +136,9 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
type="checkbox" type="checkbox"
checked={hardStopEnabled} checked={hardStopEnabled}
onChange={(e) => setHardStopEnabled(e.target.checked)} 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>
<label className="flex items-center gap-2.5"> <label className="flex items-center gap-2.5">
@@ -146,15 +146,15 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
type="checkbox" type="checkbox"
checked={notifyEnabled} checked={notifyEnabled}
onChange={(e) => setNotifyEnabled(e.target.checked)} 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> </label>
<div className="flex items-center justify-end gap-3 pt-2"> <div className="flex items-center justify-end gap-3 pt-2">
<button <button
type="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} onClick={onClose}
> >
Cancel Cancel
@@ -91,7 +91,7 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
return ( return (
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between px-4 py-3"> <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} /> <ActionButton label="Add Policy" icon={Plus} onClick={handleAdd} />
</div> </div>
<div className="flex-1 overflow-y-auto px-4 pb-4"> <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) => { {policies.map((policy) => {
const util = getUtilization(policy.id); const util = getUtilization(policy.id);
const pct = util?.pct ?? 0; 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 ( return (
<ListRow key={policy.id} onClick={() => handleEdit(policy)}> <ListRow key={policy.id} onClick={() => handleEdit(policy)}>
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <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} {policy.scopeType}
</span> </span>
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{getScopeName(policy)}</span> <span className="truncate text-[13px] font-medium text-foreground">{getScopeName(policy)}</span>
{policy.hardStopEnabled ? <ShieldCheck size={14} className="shrink-0 text-[color:var(--accent)]" /> : null} {policy.hardStopEnabled ? <ShieldCheck size={14} className="shrink-0 text-primary" /> : null}
{util?.hasIncident ? <Warning size={14} weight="fill" className="shrink-0 text-[color:var(--warn)]" /> : null} {util?.hasIncident ? <Warning size={14} weight="fill" className="shrink-0 text-yellow-600 dark:text-yellow-400" /> : null}
</div> </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" }}> <span className="font-[family-name:var(--font-mono)]" style={{ fontVariantNumeric: "tabular-nums" }}>
${(policy.amount / 100).toFixed(2)}/mo ${(policy.amount / 100).toFixed(2)}/mo
</span> </span>
@@ -120,10 +120,10 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
</div> </div>
{util ? ( {util ? (
<div className="mt-1.5 flex items-center gap-2"> <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 className={`h-1.5 rounded-full ${barColor}`} style={{ width: `${Math.min(100, pct)}%` }} />
</div> </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)}% {Math.round(pct)}%
</span> </span>
</div> </div>
@@ -131,7 +131,7 @@ export function PolicyList({ companyId, agents, projects }: PolicyListProps) {
</div> </div>
<button <button
type="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" title="Delete policy"
aria-label="Delete policy" aria-label="Delete policy"
onClick={(e) => { onClick={(e) => {
@@ -20,12 +20,12 @@ export function SecretCreator({ companyId, onSaved }: { companyId: string; onSav
return ( return (
<div className="grid gap-4"> <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" /> <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} {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} /> <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> </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) }} /> <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 ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : 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-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-[color:var(--muted)]">{t("exec.preferences")}</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.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") }]} /> <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="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> </div>
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; return ( {(() => { 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="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-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-[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.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>) })()}
<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-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-[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-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-[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.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> </div>
) )
} }
@@ -73,7 +73,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
<div <div
role="region" role="region"
aria-label="Social post draft preview" 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` }} style={{ borderColor: `${platformColor}4D` }}
> >
<div className="mb-2 flex items-center gap-2"> <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"> <div className="mb-3 flex items-center gap-2">
<PlatformIcon size={16} style={{ color: platformColor }} /> <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} {config.label}
</span> </span>
{draft.socialAccountId ? ( {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)} @{draft.socialAccountId.slice(0, 8)}
</span> </span>
) : null} ) : null}
</div> </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} {draft.content}
</div> </div>
<div <div
className="mb-2 text-[10px] font-[family-name:var(--font-mono)]" 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" aria-live="polite"
> >
{charCount} / {charLimit}{isOverLimit ? " -- over limit" : ""} {charCount} / {charLimit}{isOverLimit ? " -- over limit" : ""}
@@ -112,7 +112,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
{draft.mediaUrls.length > 0 ? ( {draft.mediaUrls.length > 0 ? (
<div className="mb-2 space-y-1"> <div className="mb-2 space-y-1">
{draft.mediaUrls.map((url, idx) => ( {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} /> <LinkIcon size={12} />
<span className="truncate max-w-[300px]">{url}</span> <span className="truncate max-w-[300px]">{url}</span>
</div> </div>
@@ -121,7 +121,7 @@ export function SocialDraftPreview({ payloadJson, platform, isPending: _isPendin
) : null} ) : null}
{draft.scheduledAt ? ( {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} /> <Clock size={14} />
Scheduled: {formatScheduledDate(draft.scheduledAt)} Scheduled: {formatScheduledDate(draft.scheduledAt)}
</div> </div>
@@ -26,25 +26,25 @@ export function TaskComments({ taskId, companyId, agents, comments, onRefresh }:
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Robot size={16} className="text-[color:var(--muted)]" /> <Robot size={16} className="text-muted-foreground" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("exec.communication")}</span> <span className="text-[13px] font-semibold text-foreground">{t("exec.communication")}</span>
<span className="text-[11px] text-[color:var(--muted)]">({comments.length})</span> <span className="text-[11px] text-muted-foreground">({comments.length})</span>
</div> </div>
{comments.length === 0 ? ( {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) => { {comments.map((comment) => {
const isAgent = Boolean(comment.authorAgentId) const isAgent = Boolean(comment.authorAgentId)
const agent = isAgent ? agents.find((a) => a.id === comment.authorAgentId) : null const agent = isAgent ? agents.find((a) => a.id === comment.authorAgentId) : null
return ( 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"> <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> <span className={`text-[12px] font-semibold ${isAgent ? "text-primary" : "text-foreground"}`}>{comment.authorName}</span>
{agent && <span className="text-[10px] text-[color:var(--muted)]">{agent.role}</span>} {agent && <span className="text-[10px] text-muted-foreground">{agent.role}</span>}
<span className="text-[10px] text-[color:var(--muted)]">{formatTime(comment.createdAt)}</span> <span className="text-[10px] text-muted-foreground">{formatTime(comment.createdAt)}</span>
</div> </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> </div>
) )
})} })}
@@ -52,7 +52,7 @@ export function TaskComments({ taskId, companyId, agents, comments, onRefresh }:
)} )}
{submitError ? <InlineNotice message={submitError} /> : null} {submitError ? <InlineNotice message={submitError} /> : null}
<div className="flex gap-2"> <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} /> <ActionButton label={t("exec.send")} tone="accent" onClick={() => void handleSubmit()} icon={ArrowRight} loading={submitting} />
</div> </div>
</div> </div>
@@ -38,7 +38,7 @@ export function TaskInspector({ task, tasks, runs, agents, goals, projects, work
return ( return (
<div className="space-y-6"> <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) }} /> <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 ? <InlineNotice message={actionError} /> : null}
{!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null} {!actionError && actionNotice ? <InlineNotice message={actionNotice} tone="success" /> : null}
{editing ? ( {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"> <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>} {assignee && <div className="text-muted-foreground">Assignee: <span className="text-foreground">{assignee.name}</span></div>}
{project && <div className="text-[color:var(--muted)]">Project: <span className="text-[color:var(--text)]">{project.name}</span></div>} {project && <div className="text-muted-foreground">Project: <span className="text-foreground">{project.name}</span></div>}
{goal && <div className="text-[color:var(--muted)]">Goal: <span className="text-[color:var(--text)]">{goal.title}</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-[color:var(--muted)]">Parent: <span className="text-[color:var(--text)]">{parent.title}</span></div> : null })()} {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-[color:var(--muted)]">Workspace: <span className="text-[color:var(--text)]">{workspace.name}</span></div>} {workspace && <div className="text-muted-foreground">Workspace: <span className="text-foreground">{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>} {task.costUsd > 0 && <div className="text-muted-foreground">Cost: <span className="font-mono tabular-nums text-foreground">{formatMoney(task.costUsd)}</span></div>}
</div> </div>
{task.description ? <div className="text-[13px] leading-relaxed text-[color:var(--muted-strong)] whitespace-pre-wrap">{task.description}</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-[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> {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-[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} : 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"> <div className="flex items-center gap-3">
{activeRun ? <ActionButton label={t("exec.cancelRun")} onClick={() => void onCancelRun(activeRun.id)} icon={XCircle} tone="danger" /> : <> {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" />} {["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 ( {(() => { const subtasks = tasks.filter((t) => t.parentId === task.id); if (subtasks.length === 0) return null; return (
<div className="space-y-2"> <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="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-[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="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 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)]"> <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-[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={`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-[color:var(--muted)]" : "text-[color:var(--text)]"}`}>{sub.title}</span> <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-[color:var(--muted)]">{subAssignee.name}</span> : null}<StatusPill status={sub.status} /> {subAssignee ? <span className="shrink-0 text-[10px] text-muted-foreground">{subAssignee.name}</span> : null}<StatusPill status={sub.status} />
</div>) })}</div></div>) })()} </div>) })}</div></div>) })()}
<TaskComments taskId={task.id} companyId={task.companyId} agents={agents} comments={comments} onRefresh={onRefresh} /> <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 ( {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-[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 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-[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> <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-[color:var(--danger-soft)] px-2 py-1 text-[11px] text-[color:var(--danger)] break-words">{run.errorMessage}</div> : null} {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-[color:var(--muted)] text-center">+{runs.length - 8} more runs</div>}</div></div>} </div>) })}{runs.length > 8 && <div className="text-[11px] text-muted-foreground text-center">+{runs.length - 8} more runs</div>}</div></div>}
</div> </div>
) )
} }
@@ -22,7 +22,7 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
return ( return (
<div <div
ref={parentRef} ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]" className="h-full overflow-auto border-y border-border"
role="listbox" role="listbox"
aria-label="Task queue" aria-label="Task queue"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -52,18 +52,18 @@ export function TaskQueue({ tasks, runs, selectedTaskId, onSelect }: { tasks: Ta
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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 <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)} onClick={() => onSelect(task.id)}
tabIndex={-1} 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 items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-2.5"> <div className="flex min-w-0 items-center gap-2.5">
<PriorityDot priority={task.priority} /> <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> </div>
<StatusPill status={activeRun?.status ?? task.status} /> <StatusPill status={activeRun?.status ?? task.status} />
</div> </div>
@@ -15,18 +15,18 @@ export function WorkspaceInspector({ workspace, agents, tasks, runs, documents,
return ( return (
<div className="space-y-5"> <div className="space-y-5">
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4"> <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-[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="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>
<div className="grid gap-3 md:grid-cols-4"> <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) => ( {[{ 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>
<div className="grid gap-4 xl:grid-cols-3"> <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-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-[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-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-[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.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>
</div> </div>
) )
@@ -24,11 +24,11 @@ export function WorkspaceList({ workspaces, projects, companyId, selectedWorkspa
const linkedProject = projects.find((p) => p.id === workspace.projectId) ?? null const linkedProject = projects.find((p) => p.id === workspace.projectId) ?? null
return ( return (
<ListRow key={workspace.id}> <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 className="flex items-center justify-between">
<div> <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="flex items-center gap-2"><div className="truncate text-[13px] text-foreground">{workspace.name}</div><WorkspaceStatePill state={workspace.state} /></div>
<div className="mono truncate text-[11px] text-[color:var(--muted)]">{workspace.localPath}</div> <div className="font-mono truncate text-[11px] text-muted-foreground">{workspace.localPath}</div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{workspace.isPrimary ? <StatusPill status="ready" /> : null} {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" /> <ActionButton label={t("exec.delete")} onClick={() => setConfirmDeleteId(workspace.id)} icon={Trash} tone="danger" />
</div> </div>
</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> </div>
</ListRow> </ListRow>
) )
@@ -27,11 +27,11 @@ export const ENTITY_TYPE_OPTIONS = [
export function WorkspaceStatePill({ state }: { state: string }) { export function WorkspaceStatePill({ state }: { state: string }) {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
ready: "text-[color:var(--success)] bg-[color:var(--success-soft)]", ready: "text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/50",
busy: "text-[color:var(--warn)] bg-[color:var(--warn-soft)]", busy: "text-yellow-600 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/50",
blocked: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]", blocked: "text-destructive bg-red-100 dark:bg-red-900/50",
missing: "text-[color:var(--muted-strong)] bg-[color:var(--panel-soft)]", missing: "text-muted-foreground bg-muted",
error: "text-[color:var(--danger)] bg-[color:var(--danger-soft)]", error: "text-destructive bg-red-100 dark:bg-red-900/50",
} }
return ( 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}`}> <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 }) { 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`} /> 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 <button
onClick={() => setFilterStage(filterStage === stage ? "all" : stage)} onClick={() => setFilterStage(filterStage === stage ? "all" : stage)}
className={`px-3 py-2 rounded-md text-xs font-medium transition-colors whitespace-nowrap ${ 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)}`} } ${stageColor(stage)}`}
> >
{stageLabelMap[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]} {stageCounts[stage]}
</span> </span>
</button> </button>
{idx < STAGE_ORDER.length - 2 && ( {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> </div>
))} ))}
@@ -104,11 +104,11 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
<button <button
onClick={() => setFilterStage(filterStage === "rejected" ? "all" : "rejected")} onClick={() => setFilterStage(filterStage === "rejected" ? "all" : "rejected")}
className={`ml-4 px-3 py-2 rounded-md text-xs font-medium transition-colors ${ 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")}`} } ${stageColor("rejected")}`}
> >
{stageLabelMap.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} {stageCounts.rejected}
</span> </span>
</button> </button>
@@ -118,7 +118,7 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
{pendingCount > 0 && ( {pendingCount > 0 && (
<Panel title={t("hiring.pendingApproval")}> <Panel title={t("hiring.pendingApproval")}>
<div className="divide-y divide-[color:var(--line)]"> <div className="divide-y divide-[color:var(--border)]">
{allCandidates {allCandidates
.filter(c => c.stage === "requisition" || c.stage === "pending_approval") .filter(c => c.stage === "requisition" || c.stage === "pending_approval")
.map((candidate) => { .map((candidate) => {
@@ -126,21 +126,21 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
const ag = candidate.agent; const ag = candidate.agent;
return ( return (
<div key={a?.id ?? ag?.id} className="px-4 py-3 flex items-start gap-3"> <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"> <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-[color:var(--warn)]" /> <UserPlus size={16} className="text-yellow-600 dark:text-yellow-400" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <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)}`}> <span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabelMap[candidate.stage]} {stageLabelMap[candidate.stage]}
</span> </span>
</div> </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} {ag ? `${ag.role} — ${ag.title}` : a?.payloadSummary}
</div> </div>
{ag?.capabilities && ( {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>
<div className="flex items-center gap-1.5 flex-shrink-0"> <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 ? ( {filteredCandidates.length === 0 ? (
<EmptyState title={t("panel.noData")} /> <EmptyState title={t("panel.noData")} />
) : ( ) : (
<div className="divide-y divide-[color:var(--line)]"> <div className="divide-y divide-[color:var(--border)]">
{filteredCandidates.map((candidate) => { {filteredCandidates.map((candidate) => {
const ag = candidate.agent; const ag = candidate.agent;
if (!ag) return null; if (!ag) return null;
@@ -206,32 +206,32 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
return ( return (
<div <div
key={ag.id} 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)} 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)} {getAgentInitials(ag.name)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <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)}`}> <span className={`text-[10px] px-1.5 py-0.5 rounded ${stageColor(candidate.stage)}`}>
{stageLabelMap[candidate.stage]} {stageLabelMap[candidate.stage]}
</span> </span>
<StatusPill status={ag.status} /> <StatusPill status={ag.status} />
</div> </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}` : ""} {ag.role} — {ag.title}{ag.department ? ` | ${ag.department}` : ""}
</div> </div>
</div> </div>
<div className="flex-shrink-0 text-right"> <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")} {manager ? `${t("hiring.reportsTo")} ${manager.name}` : t("hiring.topLevel")}
</div> </div>
{requestedBy && ( {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>
</div> </div>
); );
@@ -241,23 +241,23 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
</Panel> </Panel>
<div className="grid grid-cols-4 gap-3"> <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="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.totalAgents")}</div> <div className="text-[10px] uppercase text-muted-foreground 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="text-2xl font-bold text-foreground mt-1">{agents.filter(a => a.status !== "terminated").length}</div>
</div> </div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]"> <div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.pendingHires")}</div> <div className="text-[10px] uppercase text-muted-foreground 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="text-2xl font-bold text-yellow-600 dark:text-yellow-400 mt-1">{agents.filter(a => a.status === "pending_approval").length}</div>
</div> </div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]"> <div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.departments")}</div> <div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.departments")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1"> <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} {new Set(agents.filter(a => a.department && a.status !== "terminated").map(a => a.department)).size}
</div> </div>
</div> </div>
<div className="rounded-lg px-4 py-3 border border-[color:var(--line)] bg-[color:var(--panel)]"> <div className="rounded-lg px-4 py-3 border border-border bg-card">
<div className="text-[10px] uppercase text-[color:var(--muted)] tracking-wider">{t("hiring.monthlyPayroll")}</div> <div className="text-[10px] uppercase text-muted-foreground tracking-wider">{t("hiring.monthlyPayroll")}</div>
<div className="text-2xl font-bold text-[color:var(--text)] mt-1"> <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()} ${agents.filter(a => a.status !== "terminated" && a.status !== "pending_approval").reduce((s, a) => s + a.budgetMonthlyUsd, 0).toLocaleString()}
</div> </div>
</div> </div>
@@ -271,15 +271,15 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
const active = deptAgents.filter(a => a.status !== "pending_approval").length; const active = deptAgents.filter(a => a.status !== "pending_approval").length;
if (active === 0 && pending === 0) return null; if (active === 0 && pending === 0) return null;
return ( return (
<div key={dept} className="bg-[color:var(--panel-soft)] rounded-md px-3 py-2"> <div key={dept} className="bg-muted rounded-md px-3 py-2">
<div className="text-xs font-medium text-[color:var(--text)] capitalize">{dept.replace("_", " ")}</div> <div className="text-xs font-medium text-foreground capitalize">{dept.replace("_", " ")}</div>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<span className="text-sm font-bold text-[color:var(--text)]">{active}</span> <span className="text-sm font-bold text-foreground">{active}</span>
<span className="text-[10px] text-[color:var(--muted)]">{t("hiring.active")}</span> <span className="text-[10px] text-muted-foreground">{t("hiring.active")}</span>
{pending > 0 && ( {pending > 0 && (
<> <>
<span className="text-sm font-bold text-[color:var(--warn)]">+{pending}</span> <span className="text-sm font-bold text-yellow-600 dark:text-yellow-400">+{pending}</span>
<span className="text-[10px] text-[color:var(--warn)]">{t("hiring.pendingApproval")}</span> <span className="text-[10px] text-yellow-600 dark:text-yellow-400">{t("hiring.pendingApproval")}</span>
</> </>
)} )}
</div> </div>
@@ -24,12 +24,12 @@ export function getHiringStage(agent: AgentRecord, approval: ApprovalRecord | nu
export function stageColor(stage: HiringStage): string { export function stageColor(stage: HiringStage): string {
switch (stage) { switch (stage) {
case "requisition": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"; case "requisition": return "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400";
case "pending_approval": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"; case "pending_approval": return "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400";
case "approved": return "bg-[color:var(--success-soft)] text-[color:var(--success)]"; case "approved": return "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400";
case "onboarding": return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"; case "onboarding": return "bg-accent text-primary";
case "active": return "bg-[color:var(--success-soft)] text-[color:var(--success)]"; case "active": return "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400";
case "rejected": return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"; 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="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <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)]"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Bell size={20} weight="fill" className="text-[color:var(--accent)]" /> <Bell size={20} weight="fill" className="text-primary" />
</div> </div>
<div> <div>
<h2 className="text-[18px] font-bold tracking-[-0.02em] text-[color:var(--text)]">{t("inbox.title")}</h2> <h2 className="text-[18px] font-bold tracking-[-0.02em] text-foreground">{t("inbox.title")}</h2>
<p className="text-[12px] text-[color:var(--muted)]"> <p className="text-[12px] text-muted-foreground">
{counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention {counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention
</p> </p>
</div> </div>
@@ -81,14 +81,14 @@ export function Inbox({ items, onNavigate }: InboxProps) {
onClick={() => setFilter(btn.key)} onClick={() => setFilter(btn.key)}
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${ className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11px] font-medium transition ${
filter === btn.key filter === btn.key
? "bg-[color:var(--accent)] text-[color:var(--text-on-accent)]" ? "bg-primary text-primary-foreground"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)] hover:bg-[color:var(--line)]" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"
}`} }`}
> >
{btn.label} {btn.label}
{btn.count > 0 ? ( {btn.count > 0 ? (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${ <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} {btn.count}
</span> </span>
@@ -98,10 +98,10 @@ export function Inbox({ items, onNavigate }: InboxProps) {
</div> </div>
{filteredItems.length === 0 ? ( {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"> <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-[color:var(--muted)]" /> <Tray size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("inbox.allClear")}</div> <div className="text-[14px] font-medium text-muted-foreground">{t("inbox.allClear")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("inbox.noItemsNow")}</div> <div className="mt-1 text-[12px] text-muted-foreground">{t("inbox.noItemsNow")}</div>
</div> </div>
) : ( ) : (
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -115,10 +115,10 @@ export function Inbox({ items, onNavigate }: InboxProps) {
initial={{ opacity: 0, y: -8 }} initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 20 }} 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 <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 }} style={{ backgroundColor: config.bg }}
> >
<Icon size={18} weight="fill" style={{ color: config.color }} /> <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" className="min-w-0 flex-1 text-left"
> >
<div className="flex items-center gap-2"> <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 <span
className="shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" 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 }} style={{ backgroundColor: config.bg, color: config.color }}
@@ -137,7 +137,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
{config.label} {config.label}
</span> </span>
</div> </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 ? ( {item.agentName ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Robot size={10} /> <Robot size={10} />
@@ -148,7 +148,7 @@ export function Inbox({ items, onNavigate }: InboxProps) {
<span className="shrink-0 tabular-nums">{timeAgo(item.createdAt)}</span> <span className="shrink-0 tabular-nums">{timeAgo(item.createdAt)}</span>
</div> </div>
</button> </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> </motion.div>
); );
})} })}
@@ -13,10 +13,10 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
const t = useT(); const t = useT();
if (!metrics) { if (!metrics) {
return ( return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center"> <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-[color:var(--muted)]" /> <Target size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("perf.noMetrics")}</div> <div className="text-[14px] font-medium text-muted-foreground">{t("perf.noMetrics")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("perf.noMetricsDesc")}</div> <div className="mt-1 text-[12px] text-muted-foreground">{t("perf.noMetricsDesc")}</div>
</div> </div>
); );
} }
@@ -48,8 +48,8 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
/> />
</div> </div>
<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="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Run Activity (30d)</div> <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 items-center gap-6">
<div className="flex-1"> <div className="flex-1">
<RunProgressBar <RunProgressBar
@@ -60,29 +60,29 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
</div> </div>
<div className="flex gap-4 text-[12px]"> <div className="flex gap-4 text-[12px]">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" /> <span className="h-2.5 w-2.5 rounded-full bg-green-600 dark:bg-green-400" />
<span className="text-[color:var(--muted)]">{metrics.successfulRuns} succeeded</span> <span className="text-muted-foreground">{metrics.successfulRuns} succeeded</span>
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[color:var(--danger)]" /> <span className="h-2.5 w-2.5 rounded-full bg-destructive" />
<span className="text-[color:var(--muted)]">{metrics.failedRuns} failed</span> <span className="text-muted-foreground">{metrics.failedRuns} failed</span>
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<section> <section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agent Performance</div> <div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Agent Performance</div>
<div className="overflow-x-auto rounded-[8px] border border-[color:var(--line)]"> <div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full text-left"> <table className="w-full text-left">
<thead> <thead>
<tr className="border-b border-[color:var(--line)] bg-[color:var(--panel-soft)]"> <tr className="border-b border-border bg-muted">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">Agent</th> <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-[color:var(--muted)] 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">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-muted-foreground 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-muted-foreground 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-muted-foreground 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> <th className="px-3 py-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-muted-foreground text-right">Cost</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -91,30 +91,30 @@ export function PerformanceView({ metrics, onNavigate }: PerformanceProps) {
.map((am) => ( .map((am) => (
<tr <tr
key={am.agentId} 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)} onClick={() => onNavigate("agents", am.agentId)}
> >
<td className="px-4 py-2.5"> <td className="px-4 py-2.5">
<div className="flex items-center gap-2"> <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)} {getAgentInitials(am.agentName)}
</div> </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> </div>
</td> </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"> <td className="px-3 py-2.5 text-right">
<span className={`text-[12px] font-semibold ${ <span className={`text-[12px] font-semibold ${
am.successRate >= 80 ? "text-[color:var(--success)]" am.successRate >= 80 ? "text-green-600 dark:text-green-400"
: am.successRate >= 50 ? "text-[color:var(--warn)]" : am.successRate >= 50 ? "text-yellow-600 dark:text-yellow-400"
: "text-[color:var(--danger)]" : "text-destructive"
}`}> }`}>
{am.successRate}% {am.successRate}%
</span> </span>
</td> </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-muted-foreground">{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-muted-foreground">{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">{formatMoney(am.totalCostUsd)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
+40 -40
View File
@@ -18,60 +18,60 @@ export function StandupView({ report, onNavigate }: StandupProps) {
const t = useT(); const t = useT();
if (!report) { if (!report) {
return ( return (
<div className="flex flex-col items-center justify-center rounded-[8px] border border-dashed border-[color:var(--line)] py-16 text-center"> <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-[color:var(--muted)]" /> <Clock size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("standup.noData")}</div> <div className="text-[14px] font-medium text-muted-foreground">{t("standup.noData")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">{t("standup.noDataDesc")}</div> <div className="mt-1 text-[12px] text-muted-foreground">{t("standup.noDataDesc")}</div>
</div> </div>
); );
} }
return ( return (
<div className="space-y-6"> <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 items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[color:var(--accent-soft)]"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Heartbeat size={20} weight="fill" className="text-[color:var(--accent)]" /> <Heartbeat size={20} weight="fill" className="text-primary" />
</div> </div>
<div> <div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("standup.dailyStandup")}</h2> <h2 className="text-[16px] font-bold text-foreground">{t("standup.dailyStandup")}</h2>
<p className="text-[12px] text-[color:var(--muted)]"> <p className="text-[12px] text-muted-foreground">
Generated {new Date(report.generatedAt).toLocaleString()} Generated {new Date(report.generatedAt).toLocaleString()}
</p> </p>
</div> </div>
</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="mt-3 flex gap-4">
<div className="text-center"> <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-[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-[color:var(--muted)]">{t("standup.productive")}</div> <div className="text-[10px] text-muted-foreground">{t("standup.productive")}</div>
</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-center">
<div className="text-[18px] font-bold tabular-nums text-[color:var(--danger)]">{report.blockers.length}</div> <div className="text-[18px] font-bold tabular-nums text-destructive">{report.blockers.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">{t("standup.blockers")}</div> <div className="text-[10px] text-muted-foreground">{t("standup.blockers")}</div>
</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-center">
<div className="text-[18px] font-bold tabular-nums text-[color:var(--warn)]">{report.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-[color:var(--muted)]">{t("standup.pendingApprovals")}</div> <div className="text-[10px] text-muted-foreground">{t("standup.pendingApprovals")}</div>
</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-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}% {report.budgetUtilization}%
</div> </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> </div>
</div> </div>
{report.companyHighlights.length > 0 ? ( {report.companyHighlights.length > 0 ? (
<section> <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"> <div className="space-y-1.5">
{report.companyHighlights.map((h, i) => ( {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" /> <CheckCircle size={14} weight="fill" />
{h} {h}
</div> </div>
@@ -82,10 +82,10 @@ export function StandupView({ report, onNavigate }: StandupProps) {
{report.blockers.length > 0 ? ( {report.blockers.length > 0 ? (
<section> <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"> <div className="space-y-1.5">
{report.blockers.map((b, i) => ( {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" /> <Warning size={14} weight="fill" />
{b} {b}
</div> </div>
@@ -95,46 +95,46 @@ export function StandupView({ report, onNavigate }: StandupProps) {
) : null} ) : null}
<section> <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"> <div className="space-y-2">
{report.agentReports.map((ar) => ( {report.agentReports.map((ar) => (
<button <button
key={ar.agentId} key={ar.agentId}
type="button" type="button"
onClick={() => onNavigate("agents", ar.agentId)} 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="relative">
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[12px] font-bold ${ <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 === "running" ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: ar.status === "error" ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : ar.status === "error" ? "bg-red-100 dark:bg-red-900/50 text-destructive"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]" : "bg-muted text-muted-foreground"
}`}> }`}>
{getAgentInitials(ar.agentName)} {getAgentInitials(ar.agentName)}
</div> </div>
{ar.status === "running" ? ( {ar.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3 w-3"> <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="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-[color:var(--success)]" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-green-600 dark:bg-green-400" />
</span> </span>
) : null} ) : null}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[color:var(--text)]">{ar.agentName}</span> <span className="text-[13px] font-semibold text-foreground">{ar.agentName}</span>
<span className="text-[10px] text-[color:var(--muted)]">{ar.role}</span> <span className="text-[10px] text-muted-foreground">{ar.role}</span>
</div> </div>
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-[color:var(--muted)]"> <div className="mt-1 flex flex-wrap gap-3 text-[11px] text-muted-foreground">
{ar.tasksInProgress > 0 ? <span className="text-[color:var(--accent)]">{ar.tasksInProgress} in progress</span> : null} {ar.tasksInProgress > 0 ? <span className="text-primary">{ar.tasksInProgress} in progress</span> : null}
{ar.tasksCompleted24h > 0 ? <span className="text-[color:var(--success)]">{ar.tasksCompleted24h} done</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-[color:var(--danger)]">{ar.tasksBlocked} blocked</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} {ar.costLast24h > 0 ? <span className="tabular-nums">${ar.costLast24h.toFixed(2)}</span> : null}
</div> </div>
{ar.highlights.length > 0 ? ( {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} ) : null}
</div> </div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" /> <ArrowRight size={14} className="shrink-0 text-muted-foreground" />
</button> </button>
))} ))}
</div> </div>
+22 -22
View File
@@ -10,19 +10,19 @@ import {
import type { InboxItemType } from "@shared/types"; import type { InboxItemType } from "@shared/types";
export const typeConfig: Record<InboxItemType, { icon: React.ElementType; color: string; bg: string; label: string }> = { 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" }, pending_approval: { icon: ShieldCheck, color: "var(--destructive)", bg: "var(--muted-foreground)", label: "Approval" },
failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" }, failed_run: { icon: XCircle, color: "var(--destructive)", bg: "var(--destructive)", label: "Failed Run" },
budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" }, budget_warning: { icon: CurrencyCircleDollar, color: "var(--destructive)", bg: "var(--muted-foreground)", label: "Budget" },
blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" }, blocked_task: { icon: Warning, color: "var(--destructive)", bg: "var(--destructive)", label: "Blocked" },
review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" }, review_needed: { icon: Eye, color: "var(--primary)", bg: "var(--primary)", label: "Review" },
agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" }, agent_message: { icon: ChatCircle, color: "var(--primary)", bg: "var(--primary)", label: "Message" },
}; };
export const severityColors: Record<string, string> = { export const severityColors: Record<string, string> = {
critical: "border-l-[color:var(--danger)]", critical: "border-l-[color:var(--destructive)]",
high: "border-l-[color:var(--warn)]", high: "border-l-[color:var(--destructive)]",
medium: "border-l-[color:var(--accent)]", medium: "border-l-[color:var(--primary)]",
low: "border-l-[color:var(--line)]", low: "border-l-[color:var(--border)]",
}; };
export function CompactMetric({ label, value, tone = "default", subtitle }: { export function CompactMetric({ label, value, tone = "default", subtitle }: {
@@ -32,37 +32,37 @@ export function CompactMetric({ label, value, tone = "default", subtitle }: {
subtitle?: string; subtitle?: string;
}) { }) {
const toneMap = { const toneMap = {
default: "text-[color:var(--text)]", default: "text-foreground",
success: "text-[color:var(--success)]", success: "text-green-600 dark:text-green-400",
warn: "text-[color:var(--warn)]", warn: "text-yellow-600 dark:text-yellow-400",
danger: "text-[color:var(--danger)]", danger: "text-destructive",
accent: "text-[color:var(--accent)]", accent: "text-primary",
}; };
return ( return (
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3"> <div className="rounded-lg border border-border bg-card px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{label}</div> <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> <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> </div>
); );
} }
export function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) { 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 successPct = (succeeded / total) * 100;
const failPct = (failed / total) * 100; const failPct = (failed / total) * 100;
return ( 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 <motion.div
className="h-full bg-[color:var(--success)]" className="h-full bg-green-600 dark:bg-green-400"
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${successPct}%` }} animate={{ width: `${successPct}%` }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.8, ease: "easeOut" }}
/> />
<motion.div <motion.div
className="h-full bg-[color:var(--danger)]" className="h-full bg-destructive"
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${failPct}%` }} animate={{ width: `${failPct}%` }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }} 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 }))} options={executionReadyConnectors.map((connector) => ({ value: connector.id, label: connector.label }))}
/> />
{executionReadyConnectors.length === 0 ? ( {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. No execution-ready connector is available. Fix connector health before requesting hires.
</p> </p>
) : null} ) : null}
@@ -127,7 +127,7 @@ export function AgentCreator({
onChange={setCapabilities} onChange={setCapabilities}
placeholder="What this agent does" 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. New agents enter the hiring pipeline first. Approval activates the agent and keeps onboarding, budgets, and org history linked to the same record.
</p> </p>
<ActionButton <ActionButton
@@ -173,7 +173,7 @@ export function AgentCreator({
icon={Robot} icon={Robot}
disabled={!selectedConnector} 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> </div>
); );
} }
@@ -128,7 +128,7 @@ export function AgentEditor({
]} ]}
/> />
{pendingApproval ? ( {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. Pending hires must be approved or rejected from Hiring or Approvals. Status changes are intentionally locked here.
</p> </p>
) : null} ) : null}
@@ -140,7 +140,7 @@ export function AgentEditor({
options={connectorOptions} options={connectorOptions}
/> />
{selectedConnectorIssue ? ( {selectedConnectorIssue ? (
<p className="text-[12px] text-[color:var(--warn)]"> <p className="text-[12px] text-yellow-600 dark:text-yellow-400">
{selectedConnectorIssue} {selectedConnectorIssue}
</p> </p>
) : null} ) : null}
@@ -193,8 +193,8 @@ export function AgentEditor({
<HeartbeatSection agent={agent} companyId={companyId} pendingApproval={pendingApproval} persistedConnectorIssue={persistedConnectorIssue} onSaved={onSaved} /> <HeartbeatSection agent={agent} companyId={companyId} pendingApproval={pendingApproval} persistedConnectorIssue={persistedConnectorIssue} onSaved={onSaved} />
{/* Skills section (D-04 dual access) */} {/* Skills section (D-04 dual access) */}
<div className="mt-6 border-t border-[color:var(--line)] pt-4"> <div className="mt-6 border-t border-border pt-4">
<h3 className="text-[13px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)] mb-3">Skills</h3> <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} /> <SkillAgentToggles mode="agent" companyId={companyId} agentId={agent.id} skills={skills} />
</div> </div>
@@ -271,11 +271,11 @@ export function AgentEditor({
)} )}
</div> </div>
{pendingApproval ? ( {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. Pending hires are read-only here. Use Hiring or Approvals to approve or reject the request before editing the agent profile.
</p> </p>
) : null} ) : null}
{saveError ? <p className="text-[12px] text-[color:var(--danger)]">{saveError}</p> : null} {saveError ? <p className="text-[12px] text-destructive">{saveError}</p> : null}
</div> </div>
); );
} }
@@ -38,7 +38,7 @@ export function AgentRoster({
return ( return (
<div <div
ref={parentRef} ref={parentRef}
className="h-full overflow-auto border-y border-[color:var(--line)]" className="h-full overflow-auto border-y border-border"
role="listbox" role="listbox"
aria-label="Agent roster" aria-label="Agent roster"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -73,29 +73,29 @@ export function AgentRoster({
width: "100%", width: "100%",
transform: `translateY(${virtualRow.start}px)`, 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 <button
type="button" type="button"
onClick={() => onSelect(agent.id)} onClick={() => onSelect(agent.id)}
tabIndex={-1} 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="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isSelected && ( {isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" /> <CheckCircle size={16} weight="fill" className="text-primary" />
)} )}
<div> <div>
<div className={`truncate text-[14px] font-semibold text-[color:var(--text)]`}>{agent.name}</div> <div className={`truncate text-[14px] font-semibold text-foreground`}>{agent.name}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]"> <div className="truncate text-[11px] text-muted-foreground">
{agent.role} · {agent.title || "No title"} {agent.role} · {agent.title || "No title"}
</div> </div>
</div> </div>
</div> </div>
<StatusPill status={agent.status} /> <StatusPill status={agent.status} />
</div> </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> <span>Connector: {connector?.label ?? agent.connectorId}</span>
{workspace && <span>Workspace: {workspace.name}</span>} {workspace && <span>Workspace: {workspace.name}</span>}
</div> </div>
@@ -30,19 +30,19 @@ export function GoalList({
<ListRow <ListRow
key={goal.id} key={goal.id}
onClick={() => onSelect(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="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
{isSelected && ( {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> </div>
<StatusPill status={goal.status} /> <StatusPill status={goal.status} />
</div> </div>
<div className="line-clamp-2 text-[13px] text-[color:var(--muted)]">{goal.description}</div> <div className="line-clamp-2 text-[13px] text-muted-foreground">{goal.description}</div>
<div className="mt-1 flex gap-4 text-[11px] text-[color:var(--muted)]"> <div className="mt-1 flex gap-4 text-[11px] text-muted-foreground">
{parentGoal && <span>Parent: {parentGoal.title}</span>} {parentGoal && <span>Parent: {parentGoal.title}</span>}
{ownerAgent && <span>Owner: {ownerAgent.name}</span>} {ownerAgent && <span>Owner: {ownerAgent.name}</span>}
</div> </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); }; 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 ( 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"> <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) => ( {[{ 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> </div>
<svg className="pointer-events-none absolute inset-0" style={{ width: "100%", height: "100%" }}> <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 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 midY = (y1 + y2) / 2;
const isChildRunning = child.agent.status === "running"; 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> </g>
</svg> </svg>
@@ -100,27 +100,27 @@ export function OrgChart({ agents, onSelectAgent }: { agents: AgentRecord[]; onS
const isPending = node.agent.status === "pending_approval"; const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo; const isCEO = node.children.length > 0 && !node.agent.reportsTo;
return ( 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 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-[color:var(--accent)]" : node.children.length > 0 ? "h-[3px] bg-[color:var(--warn)]" : "h-[2px] bg-[color:var(--muted)] opacity-30"}`} /> <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="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0"> <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 ? ( {isRunning ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5" aria-hidden="true"> <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="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>
) : ( ) : (
<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> <span className="sr-only">{`Status: ${node.agent.status}`}</span>
</div> </div>
<div className="flex min-w-0 flex-1 flex-col items-start"> <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="truncate text-[14px] font-semibold leading-tight text-foreground">{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="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"> <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> <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-[color:var(--success)]" /> : null} {node.agent.heartbeatEnabled ? <Heartbeat size={10} className="text-green-600 dark:text-green-400" /> : null}
</div> </div>
</div> </div>
</div> </div>
@@ -31,19 +31,19 @@ export function ProjectList({
<ListRow <ListRow
key={project.id} key={project.id}
onClick={() => onSelect(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="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isSelected && ( {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> </div>
<StatusPill status={project.status} /> <StatusPill status={project.status} />
</div> </div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">{project.description}</div> <div className="mb-2 text-[13px] text-muted-foreground">{project.description}</div>
<div className="flex flex-wrap gap-4 text-[11px] text-[color:var(--muted)]"> <div className="flex flex-wrap gap-4 text-[11px] text-muted-foreground">
<span> <span>
Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"} Goal: {goals.find((g) => g.id === project.goalId)?.title ?? "None"}
</span> </span>
@@ -8,13 +8,13 @@ export function BudgetBar({ spent, budget }: { spent: number; budget: number })
const overBudget = spent > budget && budget > 0; const overBudget = spent > budget && budget > 0;
return ( return (
<div className="flex items-center gap-2"> <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 <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}%` }} style={{ width: `${pct}%` }}
/> />
</div> </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)} {formatMoney(spent)} / {formatMoney(budget)}
</span> </span>
</div> </div>
@@ -31,8 +31,8 @@ export function ConfirmDelete({
onCancel: () => void; onCancel: () => void;
}) { }) {
return ( return (
<div className="flex items-center gap-3 rounded-[8px] border border-[color:var(--danger)] bg-[color:var(--danger-soft)] px-4 py-3"> <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-[color:var(--danger)]">Delete {entityLabel}?</span> <span className="text-[13px] text-destructive">Delete {entityLabel}?</span>
<ActionButton label="Yes, delete" tone="danger" onClick={onConfirm} icon={Trash} /> <ActionButton label="Yes, delete" tone="danger" onClick={onConfirm} icon={Trash} />
<ActionButton label="Cancel" onClick={onCancel} icon={XCircle} /> <ActionButton label="Cancel" onClick={onCancel} icon={XCircle} />
</div> </div>
@@ -63,10 +63,10 @@ export function HeartbeatSection({
onSaved: () => Promise<void>; onSaved: () => Promise<void>;
}) { }) {
return ( 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"> <div className="mb-2 flex items-center gap-2">
<Heartbeat size={16} className="text-[color:var(--accent)]" /> <Heartbeat size={16} className="text-primary" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Continuous Execution</span> <span className="text-[13px] font-semibold text-foreground">Continuous Execution</span>
</div> </div>
<div className="mb-2 flex items-center gap-3"> <div className="mb-2 flex items-center gap-3">
<label className="flex cursor-pointer items-center gap-2"> <label className="flex cursor-pointer items-center gap-2">
@@ -80,12 +80,12 @@ export function HeartbeatSection({
await onSaved(); 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> </label>
{agent.heartbeatEnabled && ( {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)}` : ""} Every {agent.heartbeatIntervalSec}s{agent.lastHeartbeatAt ? ` · Last: ${formatTime(agent.lastHeartbeatAt)}` : ""}
</span> </span>
)} )}
@@ -118,7 +118,7 @@ export function HeartbeatSection({
} }
/> />
{persistedConnectorIssue ? ( {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. {persistedConnectorIssue} Save a ready connector before waking this agent again.
</p> </p>
) : null} ) : null}
@@ -86,13 +86,13 @@ export function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; c
} }
export const statusDotColor: Record<string, string> = { export const statusDotColor: Record<string, string> = {
running: "var(--accent)", running: "var(--primary)",
active: "var(--success)", active: "var(--primary)",
paused: "var(--warn)", paused: "var(--destructive)",
idle: "var(--muted)", idle: "var(--muted-foreground)",
error: "var(--danger)", error: "var(--destructive)",
terminated: "var(--muted)", terminated: "var(--muted-foreground)",
pending_approval: "var(--warn)", pending_approval: "var(--destructive)",
}; };
export const adapterLabels: Record<string, string> = { 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 }) { function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<span className="text-[12px] text-[color:var(--muted)]">{label}</span> <span className="text-[12px] text-muted-foreground">{label}</span>
<span className="text-[13px] text-[color:var(--text)]">{children}</span> <span className="text-[13px] text-foreground">{children}</span>
</div> </div>
); );
} }
@@ -72,7 +72,7 @@ export function RunDetail({
</div> </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="Status"><StatusPill status={selectedRun.status} /></DetailRow>
<DetailRow label="Agent">{agent?.name ?? selectedRun.agentId}</DetailRow> <DetailRow label="Agent">{agent?.name ?? selectedRun.agentId}</DetailRow>
<DetailRow label="Connector">{selectedRun.connectorId}</DetailRow> <DetailRow label="Connector">{selectedRun.connectorId}</DetailRow>
@@ -82,7 +82,7 @@ export function RunDetail({
<DetailRow label="Cost">{formatMoney(selectedRun.costUsd)}</DetailRow> <DetailRow label="Cost">{formatMoney(selectedRun.costUsd)}</DetailRow>
</div> </div>
{selectedRun.errorMessage ? ( {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} {selectedRun.errorMessage}
</div> </div>
) : null} ) : null}
@@ -118,15 +118,15 @@ export function RunDetail({
)} )}
> >
{logState.error ? ( {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} {logState.error}
</div> </div>
) : null} ) : 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> <span>{logState.loading ? "Refreshing\u2026" : logState.eof ? "At latest tail" : "Tail window loaded"}</span>
</div> </div>
<div className="max-h-[420px] overflow-auto rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3"> <div className="max-h-[420px] overflow-auto rounded-lg border border-border bg-card px-4 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">
{logState.content || "No log output captured yet."} {logState.content || "No log output captured yet."}
</pre> </pre>
</div> </div>
+4 -4
View File
@@ -71,19 +71,19 @@ export function RunQueue({
<ListRow <ListRow
key={run.id} key={run.id}
onClick={() => onSelectRun(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="space-y-1.5">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-[color:var(--text)]">{title}</div> <div className="truncate text-[13px] font-semibold text-foreground">{title}</div>
<div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]"> <div className="mt-0.5 truncate text-[11px] text-muted-foreground">
{agent?.name ?? "Unknown agent"} via {run.connectorId} {workspace ? `\u00B7 ${workspace.name}` : ""} {agent?.name ?? "Unknown agent"} via {run.connectorId} {workspace ? `\u00B7 ${workspace.name}` : ""}
</div> </div>
</div> </div>
<StatusPill status={run.status} /> <StatusPill status={run.status} />
</div> </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="truncate">{supportingText}</span>
<span className="shrink-0">{timeAgo(run.updatedAt)}</span> <span className="shrink-0">{timeAgo(run.updatedAt)}</span>
</div> </div>
+8 -8
View File
@@ -45,16 +45,16 @@ export function RunSummaryCard({
tone?: "default" | "danger"; tone?: "default" | "danger";
}) { }) {
return ( 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 className="flex items-start justify-between gap-3">
<div> <div>
<div className="text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div> <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-[color:var(--danger)]" : "text-[color:var(--text)]"}`}> <div className={`mt-1 text-[28px] font-semibold tracking-[-0.03em] ${tone === "danger" ? "text-destructive" : "text-foreground"}`}>
{value} {value}
</div> </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>
<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} /> <Icon size={18} />
</div> </div>
</div> </div>
@@ -64,9 +64,9 @@ export function RunSummaryCard({
export function RunMeta({ label, value }: { label: string; value: ReactNode }) { export function RunMeta({ label, value }: { label: string; value: ReactNode }) {
return ( return (
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-3.5 py-3"> <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-[color:var(--muted)]">{label}</div> <div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{label}</div>
<div className="mt-1 text-[13px] text-[color:var(--text)]">{value}</div> <div className="mt-1 text-[13px] text-foreground">{value}</div>
</div> </div>
); );
} }
@@ -62,7 +62,7 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
if (items.length === 0) { if (items.length === 0) {
return ( 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"} {mode === "skill" ? "No agents in this company" : "No skills imported yet"}
</p> </p>
); );
@@ -78,15 +78,15 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
key={item.id} key={item.id}
type="button" type="button"
disabled={isToggling} 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)} onClick={() => void handleToggle(item.id)}
> >
{/* Toggle indicator */} {/* Toggle indicator */}
<span <span
className={`flex h-[18px] w-[32px] shrink-0 items-center rounded-full p-[2px] transition-colors ${ className={`flex h-[18px] w-[32px] shrink-0 items-center rounded-full p-[2px] transition-colors ${
isAssigned isAssigned
? "bg-[color:var(--accent)]" ? "bg-primary"
: "bg-[color:var(--line)]" : "bg-[color:var(--border)]"
}`} }`}
> >
<span <span
@@ -96,11 +96,11 @@ export function SkillAgentToggles(props: SkillAgentTogglesProps) {
/> />
</span> </span>
<div className="min-w-0 flex-1"> <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} {item.name}
</div> </div>
{item.detail ? ( {item.detail ? (
<div className="truncate text-[12px] text-[color:var(--muted)]"> <div className="truncate text-[12px] text-muted-foreground">
{item.detail} {item.detail}
</div> </div>
) : null} ) : null}
+30 -30
View File
@@ -134,7 +134,7 @@ export function SkillDetail({
if (loading) { if (loading) {
return ( 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 className="text-[13px]">Loading skill...</div>
</div> </div>
); );
@@ -142,7 +142,7 @@ export function SkillDetail({
if (!detail) { if (!detail) {
return ( 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 className="text-[13px]">Skill not found</div>
</div> </div>
); );
@@ -165,15 +165,15 @@ export function SkillDetail({
return ( return (
<div className="min-w-0"> <div className="min-w-0">
{/* Header section */} {/* 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"> <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"> <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} {detail.name}
</h1> </h1>
{detail.description ? ( {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} {detail.description}
</p> </p>
) : null} ) : null}
@@ -182,35 +182,35 @@ export function SkillDetail({
</div> </div>
{/* Metadata row */} {/* 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]"> <div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-[13px]">
{/* Source */} {/* Source */}
<div className="flex items-center gap-2"> <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"> <span className="flex items-center gap-1.5">
<SourceIcon size={14} className="text-[color:var(--muted)]" /> <SourceIcon size={14} className="text-muted-foreground" />
<span className="text-[color:var(--text)]">{sourceLabel[detail.sourceType] ?? detail.sourceType}</span> <span className="text-foreground">{sourceLabel[detail.sourceType] ?? detail.sourceType}</span>
</span> </span>
</div> </div>
{/* Trust level */} {/* Trust level */}
<div className="flex items-center gap-2"> <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"} /> <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> </div>
{/* Compatibility */} {/* Compatibility */}
<div className="flex items-center gap-2"> <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-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Compat</span>
<span className="text-[color:var(--text)]">{detail.compatibility || "all"}</span> <span className="text-foreground">{detail.compatibility || "all"}</span>
</div> </div>
{/* Ref (for GitHub sources) */} {/* Ref (for GitHub sources) */}
{isGitHubSource ? ( {isGitHubSource ? (
<div className="flex items-center gap-2"> <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="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Ref</span>
<span className="font-mono text-[12px] text-[color:var(--text)]">{shortRef(detail.sourceRef)}</span> <span className="font-mono text-[12px] text-foreground">{shortRef(detail.sourceRef)}</span>
</div> </div>
) : null} ) : null}
</div> </div>
@@ -225,11 +225,11 @@ export function SkillDetail({
onClick={() => void handleCheckUpdate()} onClick={() => void handleCheckUpdate()}
/> />
{updateStatus?.hasUpdate ? ( {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)} Update available: {shortRef(updateStatus.currentRef)} &rarr; {shortRef(updateStatus.latestRef)}
</span> </span>
) : updateStatus && !updateStatus.hasUpdate ? ( ) : 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} ) : null}
</div> </div>
) : null} ) : null}
@@ -240,7 +240,7 @@ export function SkillDetail({
{/* File tree sidebar + content */} {/* File tree sidebar + content */}
<div className="grid grid-cols-[12rem_minmax(0,1fr)]"> <div className="grid grid-cols-[12rem_minmax(0,1fr)]">
{/* File tree sidebar */} {/* File tree sidebar */}
<div className="border-r border-[color:var(--line)] py-2"> <div className="border-r border-border py-2">
{treeNodes.length > 0 ? ( {treeNodes.length > 0 ? (
<SkillFileTree <SkillFileTree
nodes={treeNodes} nodes={treeNodes}
@@ -250,18 +250,18 @@ export function SkillDetail({
onSelectPath={setSelectedSkillFilePath} 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> </div>
{/* Content area */} {/* Content area */}
<div className="p-4"> <div className="p-4">
{/* File path bar */} {/* 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) => ( {selectedSkillFilePath.split("/").map((segment, i, arr) => (
<span key={i} className="flex items-center gap-1"> <span key={i} className="flex items-center gap-1">
{i > 0 ? <span className="opacity-40">/</span> : null} {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} {segment}
</span> </span>
</span> </span>
@@ -270,35 +270,35 @@ export function SkillDetail({
{/* Content */} {/* Content */}
{isSkillMd ? ( {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} {fileContent}
</pre> </pre>
) : selectedFileEntry ? ( ) : selectedFileEntry ? (
<div className="rounded-lg bg-[color:var(--panel)] p-4 text-[13px] text-[color:var(--muted)]"> <div className="rounded-lg bg-card p-4 text-[13px] text-muted-foreground">
<p className="font-medium text-[color:var(--text)]">{selectedFileEntry.path}</p> <p className="font-medium text-foreground">{selectedFileEntry.path}</p>
<p className="mt-1">Kind: {selectedFileEntry.kind}</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> <p className="mt-1 text-[12px] italic">File content preview is not available for non-SKILL.md files.</p>
</div> </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> </div>
</div> </div>
{/* Agent assignments section */} {/* Agent assignments section */}
<div className="border-t border-[color:var(--line)] px-5 py-4"> <div className="border-t border-border px-5 py-4">
<h3 className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)] mb-3"> <h3 className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">
Agent Assignments Agent Assignments
</h3> </h3>
<SkillAgentToggles mode="skill" companyId={companyId} skillId={skillId} agents={agents} /> <SkillAgentToggles mode="skill" companyId={companyId} skillId={skillId} agents={agents} />
</div> </div>
{/* Delete section */} {/* 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 ? ( {confirmingDelete ? (
<div className="flex items-center gap-3"> <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. Delete &ldquo;{detail.name}&rdquo;? This cannot be undone.
</p> </p>
<ActionButton <ActionButton
@@ -90,7 +90,7 @@ export function SkillFileTree({
<div key={node.path ?? node.name}> <div key={node.path ?? node.name}>
<button <button
type="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` }} style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onToggleDir(node.path)} onClick={() => node.path && onToggleDir(node.path)}
> >
@@ -118,10 +118,10 @@ export function SkillFileTree({
<button <button
key={node.path ?? node.name} key={node.path ?? node.name}
type="button" 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 isSelected
? "text-[color:var(--text)] font-medium" ? "text-foreground font-medium"
: "text-[color:var(--muted)]" : "text-muted-foreground"
}`} }`}
style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }} style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onSelectPath(node.path)} onClick={() => node.path && onSelectPath(node.path)}
@@ -114,7 +114,7 @@ export function SkillImportModal({
<Modal open={open} title="Import Skill" onClose={handleClose} width="560px"> <Modal open={open} title="Import Skill" onClose={handleClose} width="560px">
<div className="space-y-5"> <div className="space-y-5">
{/* Tab bar */} {/* 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) => { {tabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
const isActive = activeTab === tab.id; const isActive = activeTab === tab.id;
@@ -124,8 +124,8 @@ export function SkillImportModal({
type="button" type="button"
className={`flex items-center gap-1.5 px-3 py-2 text-[12px] font-medium transition-colors ${ className={`flex items-center gap-1.5 px-3 py-2 text-[12px] font-medium transition-colors ${
isActive isActive
? "border-b-2 border-[color:var(--accent)] text-[color:var(--text)]" ? "border-b-2 border-[color:var(--primary)] text-foreground"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:text-foreground"
}`} }`}
onClick={() => { onClick={() => {
setActiveTab(tab.id); setActiveTab(tab.id);
@@ -144,7 +144,7 @@ export function SkillImportModal({
{activeTab === "local" ? ( {activeTab === "local" ? (
<div className="flex gap-2"> <div className="flex gap-2">
<input <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} value={source}
onChange={(e) => setSource(e.target.value)} onChange={(e) => setSource(e.target.value)}
placeholder={placeholder.local} placeholder={placeholder.local}
@@ -153,7 +153,7 @@ export function SkillImportModal({
</div> </div>
) : ( ) : (
<input <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} value={source}
onChange={(e) => setSource(e.target.value)} onChange={(e) => setSource(e.target.value)}
placeholder={placeholder[activeTab]} placeholder={placeholder[activeTab]}
@@ -161,8 +161,8 @@ export function SkillImportModal({
)} )}
{activeTab === "skills_sh" && ( {activeTab === "skills_sh" && (
<p className="text-[12px] text-[color:var(--muted)]"> <p className="text-[12px] text-muted-foreground">
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. 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> </p>
)} )}
</div> </div>
+5 -5
View File
@@ -62,7 +62,7 @@ export function SkillList({
if (filteredSkills.length === 0) { if (filteredSkills.length === 0) {
return ( 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. No skills match this filter.
</div> </div>
); );
@@ -79,10 +79,10 @@ export function SkillList({
const filePath = selectedPaths[skill.id] ?? "SKILL.md"; const filePath = selectedPaths[skill.id] ?? "SKILL.md";
return ( return (
<div key={skill.id} className="border-b border-[color:var(--line)]"> <div key={skill.id} className="border-b border-border">
<div <div
className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-[color:var(--panel-soft)] ${ className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-muted ${
isSelected ? "text-[color:var(--text)]" : "text-[color:var(--muted)]" isSelected ? "text-foreground" : "text-muted-foreground"
}`} }`}
> >
<SourceIcon <SourceIcon
@@ -101,7 +101,7 @@ export function SkillList({
</button> </button>
<button <button
type="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)} onClick={() => handleToggleSkill(skill.id)}
aria-label={isExpanded ? "Collapse file tree" : "Expand file tree"} aria-label={isExpanded ? "Collapse file tree" : "Expand file tree"}
> >
@@ -60,20 +60,20 @@ export function SkillsSection({
return ( return (
<div className="grid h-full grid-cols-1 xl:grid-cols-[19rem_minmax(0,1fr)]"> <div className="grid h-full grid-cols-1 xl:grid-cols-[19rem_minmax(0,1fr)]">
{/* Left column -- skill list sidebar */} {/* 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 */} {/* Header */}
<div className="flex shrink-0 items-center gap-3 px-4 py-3"> <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 Skills
</h2> </h2>
{skills.length > 0 && ( {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} {skills.length}
</span> </span>
)} )}
<button <button
type="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)} onClick={() => setSkillImportModalOpen(true)}
aria-label="Import skill" aria-label="Import skill"
> >
@@ -86,11 +86,11 @@ export function SkillsSection({
<div className="relative"> <div className="relative">
<MagnifyingGlass <MagnifyingGlass
size={14} 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" aria-hidden="true"
/> />
<input <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} value={skillFilter}
onChange={(e) => setSkillFilter(e.target.value)} onChange={(e) => setSkillFilter(e.target.value)}
placeholder="Filter skills..." placeholder="Filter skills..."
@@ -108,7 +108,7 @@ export function SkillsSection({
action={ action={
<button <button
type="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)} onClick={() => setSkillImportModalOpen(true)}
> >
<Plus size={14} weight="bold" /> <Plus size={14} weight="bold" />
+15 -15
View File
@@ -17,7 +17,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
const cardContent = ( const cardContent = (
<> <>
<div className="flex items-center gap-2"> <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 <span
className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]"
style={{ backgroundColor: platform.bgColor, color: platform.color }} style={{ backgroundColor: platform.bgColor, color: platform.color }}
@@ -25,10 +25,10 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
{platform.label} {platform.label}
</span> </span>
</div> </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> <span className="truncate">@{account.accountName}</span>
{account.requireApproval ? ( {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} /> <ShieldCheck size={10} />
Approval required Approval required
</span> </span>
@@ -39,9 +39,9 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
); );
return ( 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 <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 }} style={{ backgroundColor: platform.bgColor, color: platform.color }}
> >
<PlatformIcon size={18} weight="fill" /> <PlatformIcon size={18} weight="fill" />
@@ -58,18 +58,18 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<div className="flex items-center gap-2"> <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 ${ <span className={`flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-semibold ${
isLoggedIn 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" : 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" : account.status === "suspended"
? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" ? "bg-red-100 dark:bg-red-900/50 text-destructive"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" : "bg-muted text-muted-foreground"
}`}> }`}>
<span className={`h-1.5 w-1.5 rounded-full ${ <span className={`h-1.5 w-1.5 rounded-full ${
isLoggedIn ? "bg-[color:var(--success)]" isLoggedIn ? "bg-green-600 dark:bg-green-400"
: account.status === "login_required" ? "bg-[color:var(--warn)]" : account.status === "login_required" ? "bg-yellow-600 dark:bg-yellow-400"
: account.status === "suspended" ? "bg-[color:var(--danger)]" : account.status === "suspended" ? "bg-destructive"
: "bg-[color:var(--muted)]" : "bg-[color:var(--muted-foreground)]"
}`} /> }`} />
{account.status === "logged_in" ? "Logged In" : account.status === "active" ? "Active" : account.status.replace("_", " ")} {account.status === "logged_in" ? "Logged In" : account.status === "active" ? "Active" : account.status.replace("_", " ")}
</span> </span>
@@ -78,7 +78,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
<button <button
type="button" type="button"
onClick={onLogin} 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} /> <SignIn size={12} />
Login Login
@@ -89,7 +89,7 @@ export function AccountCard({ account, onLogin, onDelete, onClick }: AccountCard
type="button" type="button"
onClick={onDelete} onClick={onDelete}
aria-label="Delete account" 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} /> <Trash size={14} />
</button> </button>
@@ -18,7 +18,7 @@ interface ActionHistoryProps {
export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onNavigate }: ActionHistoryProps) { export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onNavigate }: ActionHistoryProps) {
if (actions.length === 0) { if (actions.length === 0) {
return ( 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 No browser actions yet
</div> </div>
); );
@@ -37,36 +37,36 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
return ( return (
<div <div
key={action.id} 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={ <Icon size={16} className={
isSuccess ? "text-[color:var(--success)]" isSuccess ? "text-green-600 dark:text-green-400"
: isFailed ? "text-[color:var(--danger)]" : isFailed ? "text-destructive"
: isRunning ? "text-[color:var(--accent)]" : isRunning ? "text-primary"
: "text-[color:var(--muted)]" : "text-muted-foreground"
} /> } />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <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 ? ( {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} /> <Robot size={10} />
{agent.name} {agent.name}
</span> </span>
) : null} ) : null}
</div> </div>
{action.resultSummary ? ( {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} ) : null}
{action.errorMessage ? ( {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} ) : null}
<div className="mt-1 flex flex-wrap items-center gap-2"> <div className="mt-1 flex flex-wrap items-center gap-2">
{action.taskId ? ( {action.taskId ? (
<button <button
type="button" type="button"
onClick={() => onNavigate("tasks", action.taskId!)} 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 Open task
</button> </button>
@@ -75,7 +75,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button <button
type="button" type="button"
onClick={() => onNavigate("approvals", action.approvalId!)} 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 Open approval
</button> </button>
@@ -84,7 +84,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button <button
type="button" type="button"
onClick={() => void onOpenPath(action.screenshotPath!)} 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} /> <ArrowSquareOut size={10} />
Screenshot Screenshot
@@ -94,7 +94,7 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
<button <button
type="button" type="button"
onClick={() => void onCancelAction(action.id)} 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} /> <ClockCounterClockwise size={10} />
Cancel Cancel
@@ -102,15 +102,15 @@ export function ActionHistory({ actions, agents, onOpenPath, onCancelAction, onN
) : null} ) : null}
</div> </div>
</div> </div>
<div className="shrink-0 text-[10px] text-[color:var(--muted)]"> <div className="shrink-0 text-[10px] text-muted-foreground">
{timeAgo(action.createdAt)} {timeAgo(action.createdAt)}
</div> </div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[9px] font-bold ${ <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)]" isSuccess ? "bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400"
: isFailed ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" : isFailed ? "bg-red-100 dark:bg-red-900/50 text-destructive"
: isRunning ? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" : isRunning ? "bg-accent text-primary"
: action.status === "approval_required" ? "bg-[color:var(--warn-soft)] text-[color:var(--warn)]" : action.status === "approval_required" ? "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" : "bg-muted text-muted-foreground"
}`}> }`}>
{action.status.replace("_", " ")} {action.status.replace("_", " ")}
</span> </span>
@@ -28,29 +28,29 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
initial={{ scale: 0.95, opacity: 0 }} initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }} 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()} onClick={(e) => e.stopPropagation()}
> >
<div className="mb-5 flex items-center justify-between"> <div className="mb-5 flex items-center justify-between">
<h3 className="text-[16px] font-bold text-[color:var(--text)]">Add Social Account</h3> <h3 className="text-[16px] font-bold text-foreground">Add Social Account</h3>
<button type="button" onClick={onClose} className="rounded-[6px] p-1 text-[color:var(--muted)] hover:bg-[color:var(--panel-soft)]"> <button type="button" onClick={onClose} className="rounded-lg p-1 text-muted-foreground hover:bg-muted">
<X size={18} /> <X size={18} />
</button> </button>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div> <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"> <div className="grid grid-cols-3 gap-1.5">
{(Object.entries(platformConfig) as [SocialPlatform, typeof platformConfig.twitter][]).map(([key, config]) => ( {(Object.entries(platformConfig) as [SocialPlatform, typeof platformConfig.twitter][]).map(([key, config]) => (
<button <button
key={key} key={key}
type="button" type="button"
onClick={() => setPlatform(key)} 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 platform === key
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)] text-[color:var(--accent)]" ? "border-[color:var(--primary)] bg-accent text-primary"
: "border-[color:var(--line)] text-[color:var(--muted)] hover:border-[color:var(--line-strong)]" : "border-border text-muted-foreground hover:border-border"
}`} }`}
> >
<span style={{ color: config.color }}> <span style={{ color: config.color }}>
@@ -76,17 +76,17 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
placeholder="Your display name" 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"> <label className="flex items-center gap-3">
<input <input
type="checkbox" type="checkbox"
checked={requireApproval} checked={requireApproval}
onChange={(e) => setRequireApproval(e.target.checked)} 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>
<div className="text-[12px] font-medium text-[color:var(--text)]">Require Board Approval for Posts</div> <div className="text-[12px] font-medium text-foreground">Require Board Approval for Posts</div>
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]"> <div className="mt-0.5 text-[10px] text-muted-foreground">
Agents must get approval before publishing content to this account Agents must get approval before publishing content to this account
</div> </div>
</div> </div>
@@ -98,7 +98,7 @@ export function AddAccountDialog({ onAdd, onClose }: AddAccountDialogProps) {
<button <button
type="button" type="button"
onClick={onClose} 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 Cancel
</button> </button>
+17 -17
View File
@@ -52,12 +52,12 @@ export function SocialAccountsPanel({
<div className="space-y-5"> <div className="space-y-5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <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)]"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent">
<Megaphone size={20} weight="fill" className="text-[color:var(--accent)]" /> <Megaphone size={20} weight="fill" className="text-primary" />
</div> </div>
<div> <div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">{t("social.socialMedia")}</h2> <h2 className="text-[16px] font-bold text-foreground">{t("social.socialMedia")}</h2>
<p className="text-[12px] text-[color:var(--muted)]"> <p className="text-[12px] text-muted-foreground">
{accounts.length} account{accounts.length !== 1 ? "s" : ""} connected {accounts.length} account{accounts.length !== 1 ? "s" : ""} connected
</p> </p>
</div> </div>
@@ -70,8 +70,8 @@ export function SocialAccountsPanel({
/> />
</div> </div>
<div className="rounded-[8px] border border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] px-4 py-3"> <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-[color:var(--accent)]"> <div className="flex items-start gap-2 text-[12px] text-primary">
<Browser size={16} className="mt-0.5 shrink-0" /> <Browser size={16} className="mt-0.5 shrink-0" />
<div> <div>
<div className="font-medium">How it works</div> <div className="font-medium">How it works</div>
@@ -84,14 +84,14 @@ export function SocialAccountsPanel({
</div> </div>
</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 <button
type="button" type="button"
onClick={() => setActiveTab("accounts")} 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" activeTab === "accounts"
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" ? "bg-card text-foreground shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:text-foreground"
}`} }`}
> >
Accounts ({accounts.length}) Accounts ({accounts.length})
@@ -99,10 +99,10 @@ export function SocialAccountsPanel({
<button <button
type="button" type="button"
onClick={() => setActiveTab("history")} 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" activeTab === "history"
? "bg-[color:var(--panel)] text-[color:var(--text)] shadow-sm" ? "bg-card text-foreground shadow-sm"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:text-foreground"
}`} }`}
> >
Action History ({actions.length}) Action History ({actions.length})
@@ -111,10 +111,10 @@ export function SocialAccountsPanel({
{activeTab === "accounts" ? ( {activeTab === "accounts" ? (
accounts.length === 0 ? ( 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"> <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-[color:var(--muted)]" /> <Megaphone size={32} className="mb-3 text-muted-foreground" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">{t("social.noAccounts")}</div> <div className="text-[14px] font-medium text-muted-foreground">{t("social.noAccounts")}</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]"> <div className="mt-1 text-[12px] text-muted-foreground">
{t("social.noAccountsDesc")} {t("social.noAccountsDesc")}
</div> </div>
<div className="mt-4"> <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" }) { export function Badge({ count, tone = "default" }: { count: number; tone?: "default" | "warn" | "danger" }) {
if (count <= 0) return null; 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 ( 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}`}> <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} {count > 99 ? "99+" : count}
@@ -33,17 +33,17 @@ export function SectionButton({
aria-current={active ? "page" : undefined} aria-current={active ? "page" : undefined}
aria-label={label} aria-label={label}
title={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 active
? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" ? "bg-muted text-foreground"
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:bg-muted hover:text-foreground"
}`} }`}
onClick={onClick} 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 ? ( {badge ? (
<span className={`absolute right-1 top-1 h-[6px] w-[6px] rounded-full ${ <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} ) : null}
</button> </button>
@@ -52,15 +52,15 @@ export function SectionButton({
return ( return (
<button <button
aria-current={active ? "page" : undefined} 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 active
? "bg-[color:var(--accent-soft)] text-[color:var(--text)]" ? "bg-accent text-foreground"
: "text-[color:var(--muted-strong)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]" : "text-muted-foreground hover:bg-muted hover:text-foreground"
}`} }`}
onClick={onClick} 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} {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-[color:var(--accent)]" : ""} /> <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> <span className={`text-[13px] tracking-[0.005em] ${active ? "font-bold" : "font-medium"}`}>{label}</span>
{badge ? <Badge count={badge} tone={badgeTone} /> : null} {badge ? <Badge count={badge} tone={badgeTone} /> : null}
</button> </button>
@@ -80,14 +80,14 @@ export function Panel({
children: ReactNode; children: ReactNode;
action?: ReactNode; action?: ReactNode;
className?: string; className?: string;
tone?: "plain" | "surface"; tone?: "plain" | "bg-card border border-border shadow-sm";
}) { }) {
return ( return (
<section className={`space-y-4 ${className}`}> <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> <div>
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent-text)]">{eyebrow}</div> : null} {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-[color:var(--text)]">{title}</h2> <h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">{title}</h2>
</div> </div>
{action} {action}
</div> </div>
@@ -122,21 +122,21 @@ export function Modal({
if (!open) return null; if (!open) return null;
return ( 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 <div
ref={trapRef} ref={trapRef}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={title} 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" }} style={{ maxWidth: width, maxHeight: "80vh" }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="flex shrink-0 items-center justify-between border-b border-[color:var(--line)] px-5 py-3.5"> <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-[color:var(--text)]">{title}</h2> <h2 className="text-[16px] font-bold tracking-[-0.015em] text-foreground">{title}</h2>
<button <button
type="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} onClick={onClose}
aria-label="Close" aria-label="Close"
> >
@@ -152,7 +152,7 @@ export function Modal({
export function Field({ label, children }: { label: string; children: ReactNode }) { export function Field({ label, children }: { label: string; children: ReactNode }) {
return ( return (
<label className="block"> <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} {children}
</label> </label>
); );
@@ -184,7 +184,7 @@ export function Input({
id={id} id={id}
aria-label={ariaLabel} aria-label={ariaLabel}
type={type} 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} value={value}
placeholder={placeholder} placeholder={placeholder}
readOnly={readOnly} readOnly={readOnly}
@@ -195,7 +195,7 @@ export function Input({
if (label) { if (label) {
return ( return (
<label className="block"> <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} {input}
</label> </label>
); );
@@ -222,7 +222,7 @@ export function TextArea({
}) { }) {
const textarea = ( const textarea = (
<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} value={value}
rows={rows} rows={rows}
placeholder={placeholder} placeholder={placeholder}
@@ -234,7 +234,7 @@ export function TextArea({
if (label) { if (label) {
return ( return (
<label className="block"> <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} {textarea}
</label> </label>
); );
@@ -259,17 +259,17 @@ export function Select({
}) { }) {
const select = ( const select = (
<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 compact
? "rounded-[6px] border-[color:var(--line)] px-2.5 py-1.5 text-[13px] font-medium" ? "rounded-lg border-border 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-3.5 py-2.5 text-[14px]"
} disabled:cursor-not-allowed disabled:opacity-60`} } disabled:cursor-not-allowed disabled:opacity-60`}
value={value} value={value}
disabled={disabled} disabled={disabled}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
> >
{options.map((option) => ( {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.label}
</option> </option>
))} ))}
@@ -278,7 +278,7 @@ export function Select({
if (label) { if (label) {
return ( return (
<label className="block"> <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} {select}
</label> </label>
); );
@@ -304,14 +304,14 @@ export function ActionButton({
const isDisabled = loading || disabled; const isDisabled = loading || disabled;
const toneClass = const toneClass =
tone === "accent" 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" : 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-transparent bg-destructive text-primary-foreground 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-border bg-card text-foreground hover:bg-muted";
return ( return (
<button <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} onClick={onClick}
disabled={isDisabled} disabled={isDisabled}
aria-busy={loading} 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 warning = ["auth_required", "degraded", "pending", "in_review", "todo", "queued", "not_installed"].includes(status);
const checking = status === "detected"; const checking = status === "detected";
const toneClass = positive const toneClass = positive
? "text-[color:var(--success)]" ? "text-green-600 dark:text-green-400"
: negative : negative
? "text-[color:var(--danger)]" ? "text-destructive"
: warning : warning
? "text-[color:var(--warn)]" ? "text-yellow-600 dark:text-yellow-400"
: "text-[color:var(--muted-strong)]"; : "text-muted-foreground";
const dotClass = positive const dotClass = positive
? "bg-[color:var(--success)]" ? "bg-green-600 dark:bg-green-400"
: negative : negative
? "bg-[color:var(--danger)]" ? "bg-destructive"
: warning : warning
? "bg-[color:var(--warn)]" ? "bg-yellow-600 dark:bg-yellow-400"
: "bg-[color:var(--muted)]"; : "bg-[color:var(--muted-foreground)]";
const displayLabel = checking ? "checking" : status.replaceAll("_", " "); const displayLabel = checking ? "checking" : status.replaceAll("_", " ");
return ( return (
<span className={`inline-flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] ${toneClass}`}> <span className={`inline-flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] ${toneClass}`}>
{checking ? ( {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" /> <span className={`h-2 w-2 rounded-full ${dotClass}`} aria-hidden="true" />
)} )}
@@ -373,12 +373,12 @@ export function InlineNotice({
action?: ReactNode; action?: ReactNode;
}) { }) {
const toneClass = tone === "success" 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" : tone === "warn"
? "border-[color:var(--warn)] bg-[color:var(--warn-soft)] text-[color:var(--warn)]" ? "border-[color:var(--destructive)] bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400"
: "border-[color:var(--danger)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]"; : "border-[color:var(--destructive)] bg-red-100 dark:bg-red-900/50 text-destructive";
return ( 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 ? ( {action ? (
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<span>{message}</span> <span>{message}</span>
@@ -410,17 +410,17 @@ export function ConfirmDialog({ open, title, message, errorMessage, onConfirm, o
if (!open) return null; if (!open) return null;
return ( 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 <div
ref={trapRef} ref={trapRef}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="confirm-dialog-title" 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()} onClick={e => e.stopPropagation()}
> >
<div id="confirm-dialog-title" className="text-[16px] font-semibold text-[color:var(--text)] mb-2">{title}</div> <div id="confirm-dialog-title" className="text-[16px] font-semibold text-foreground mb-2">{title}</div>
<div className="text-[13px] text-[color:var(--muted)] mb-6">{message}</div> <div className="text-[13px] text-muted-foreground mb-6">{message}</div>
{errorMessage ? <div className="mb-4"><InlineNotice message={errorMessage} /></div> : null} {errorMessage ? <div className="mb-4"><InlineNotice message={errorMessage} /></div> : null}
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<ActionButton label="Cancel" onClick={onCancel} /> <ActionButton label="Cancel" onClick={onCancel} />
@@ -442,8 +442,8 @@ export function EmptyState({
}) { }) {
return ( return (
<div className="px-1 py-8 text-center"> <div className="px-1 py-8 text-center">
<div className="mb-2 text-[15px] font-semibold text-[color:var(--text)]">{title}</div> <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-[color:var(--muted)]">{detail}</div> : null} {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} {action ? <div className="mt-5">{action}</div> : null}
</div> </div>
); );
@@ -456,7 +456,7 @@ export function ListFrame({
children: ReactNode; children: ReactNode;
className?: string; 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({ export function ListRow({
@@ -472,14 +472,14 @@ export function ListRow({
return ( return (
<button <button
type="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} onClick={onClick}
> >
{children} {children}
</button> </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) { export function useFocusTrap(active: boolean) {
@@ -550,7 +550,7 @@ export class ErrorBoundary extends Component<{ children: ReactNode; fallback?: R
if (this.state.hasError) { if (this.state.hasError) {
return this.props.fallback ?? ( return this.props.fallback ?? (
<div className="p-8 text-center" role="alert"> <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()} /> <ActionButton label="Reload page" onClick={() => window.location.reload()} />
</div> </div>
); );
+1 -1
View File
@@ -745,7 +745,7 @@ export const en: Record<string, string> = {
"ops.newMeeting": "New meeting", "ops.newMeeting": "New meeting",
"ops.newSprint": "New sprint", "ops.newSprint": "New sprint",
"ops.deleteDocument": "Delete document", "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.deleteKnowledgeEntry": "Delete knowledge entry",
"ops.deleteKnowledgeMsg": "This removes the entry from the shared company knowledge base.", "ops.deleteKnowledgeMsg": "This removes the entry from the shared company knowledge base.",
"ops.deleteMeeting": "Delete meeting", "ops.deleteMeeting": "Delete meeting",