refactor(automation): replace ACCENT/LINE constants with design tokens

This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 22:10:04 -04:00
1 parent f177e6c877
commit a443312f1a
4 files changed
+20 -26

No files matched your search

@@ -4,7 +4,7 @@ import { ArrowRight, ArrowsClockwise, Lightning, Plus, Robot, X } from "@phospho
import type { AutomationAction, AutomationRuleRecord, AutomationStatus, AutomationTrigger, WorkflowPipelineRecord } from "@shared/types"
import { ActionButton, ConfirmDialog } from "../ui"
import { useT } from "../../i18n"
import { ACCENT, LINE, triggerConfig, actionConfig, type AutomationLogEntry } from "./automation-config"
import { triggerConfig, actionConfig, type AutomationLogEntry } from "./automation-config"
import { timeAgo } from "../../lib/formatters"
import { RuleCard } from "./RuleCard"
import { RuleFormModal } from "./RuleEditor"
@@ -16,7 +16,7 @@ function AutomationActivityLog({ entries }: { entries: AutomationLogEntry[] }) {
return (
<div className="space-y-1">{entries.map((entry, idx) => {
const ti = triggerConfig[entry.trigger as AutomationTrigger]; const ai = actionConfig[entry.action as AutomationAction]; const TIcon = ti?.icon ?? Lightning
return <motion.div key={`${entry.ruleId}-${entry.executedAt}-${idx}`} initial={{ opacity: 0, x: -6 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: idx * 0.03 }} className="flex items-center gap-3 rounded-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>
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 border-border bg-card px-4 py-2.5 text-[12px]"><TIcon size={14} className="text-primary" /><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>
)
}
@@ -35,12 +35,12 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}15`, color: ACCENT }}><Lightning size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Automation Rules</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}</p></div></div><ActionButton label="New Rule" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{rules.length > 0 ? <div className="grid grid-cols-1 gap-2 sm:grid-cols-3">{[{ 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 items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary"><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-1 gap-2 sm:grid-cols-3">{[{ l: "Total Rules", v: counts.total, accent: false }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), accent: true }, { l: "Recent Activity", v: actLog.length, accent: true }].map((s) => <div key={s.l} className="rounded-lg border border-border bg-card px-4 py-3.5"><div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{s.l}</div><div className={`text-[26px] font-bold tabular-nums ${s.accent ? "text-primary" : ""}`}>{s.v}</div></div>)}</div> : null}
<div className="flex gap-1 rounded-lg bg-muted p-1"><button type="button" onClick={() => setActiveTab("rules")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Rules ({rules.length})</button><button type="button" onClick={() => setActiveTab("activity")} className={`flex-1 rounded-lg px-3 py-1.5 text-[12px] font-medium transition ${activeTab === "activity" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}>Activity Log ({actLog.length})</button></div>
{activeTab === "rules" ? <>
{rules.length > 0 ? <div className="flex flex-wrap items-center gap-1.5">{(["all","active","paused","error"] as const).map((s) => <button key={s} type="button" onClick={() => setFilterStatus(s)} className={`rounded-full px-3 py-1.5 text-[11px] font-medium transition ${filterStatus === s ? "text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`} style={filterStatus === s ? { backgroundColor: ACCENT } : undefined}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center" style={{ borderColor: LINE }}><Lightning size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
{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 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-[color:var(--border)]"}`}>{s === "all" ? "All" : s.charAt(0).toUpperCase() + s.slice(1)}</button>)}</div> : null}
{filtered.length === 0 && rules.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"><Lightning size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No automation rules</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Create your first rule to make the company autonomous.</div><div className="mt-4"><ActionButton label="Create First Rule" onClick={() => setShowCreate(true)} icon={Lightning} tone="accent" /></div></div>
: filtered.length === 0 ? <div className="py-8 text-center text-[13px] text-muted-foreground">No rules match the current filter.</div>
: <div className="space-y-2"><AnimatePresence>{filtered.map((r) => <RuleCard key={r.id} rule={r} onToggle={() => void handleToggle(r)} onDelete={() => setDelTarget(r)} onEdit={() => setEditRule(r)} />)}</AnimatePresence></div>}
</> : <AutomationActivityLog entries={actLog} />}
@@ -59,8 +59,8 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl
return (
<div className="space-y-5">
{actionError ? <div className="flex items-center justify-between rounded-lg bg-destructive-muted px-3.5 py-2.5 text-[12px] font-medium text-destructive"><span>{actionError}</span><button type="button" onClick={() => setActionError(null)} className="ml-2 rounded p-0.5 hover:bg-destructive hover:text-primary-foreground"><X size={12} /></button></div> : null}
<div className="flex items-center justify-between"><div className="flex items-center gap-3"><div className="flex h-10 w-10 items-center justify-center rounded-lg" style={{ backgroundColor: `${ACCENT}12`, color: ACCENT }}><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Workflow Pipelines</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-16 text-center" style={{ borderColor: LINE }}><ArrowsClockwise size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
<div className="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 bg-primary/10 text-primary"><ArrowsClockwise size={20} weight="fill" /></div><div><h2 className="text-[16px] font-bold text-foreground">Workflow Pipelines</h2><p className="text-[12px] text-muted-foreground">{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}</p></div></div><ActionButton label="New Workflow" onClick={() => setShowCreate(true)} icon={Plus} tone="accent" /></div>
{workflows.length === 0 ? <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"><ArrowsClockwise size={36} className="mb-3 text-muted-foreground" /><div className="text-[14px] font-medium text-muted-foreground">No workflow pipelines</div><div className="mx-auto mt-1 max-w-[42ch] text-[12px] leading-relaxed text-muted-foreground">Workflows chain actions into multi-step pipelines.</div><div className="mt-4"><ActionButton label="Create First Workflow" onClick={() => setShowCreate(true)} icon={ArrowsClockwise} tone="accent" /></div></div>
: <div className="space-y-2"><AnimatePresence>{workflows.map((wf) => <WorkflowCard key={wf.id} workflow={wf} isRunning={runningId === wf.id} onDelete={() => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}</AnimatePresence></div>}
<AnimatePresence>{showCreate ? <WorkflowFormModal open={showCreate} editingWorkflow={null} companyId={companyId} onClose={() => setShowCreate(false)} onSaved={onRefresh} /> : null}{editWf ? <WorkflowFormModal open={!!editWf} editingWorkflow={editWf} companyId={companyId} onClose={() => setEditWf(null)} onSaved={onRefresh} /> : null}</AnimatePresence>
<ConfirmDialog open={!!delTarget} title="Delete workflow" message={delTarget ? `Are you sure you want to delete ${delTarget.name}? This action cannot be undone.` : ""} onConfirm={() => void handleDelete()} onCancel={() => setDelTarget(null)} />
@@ -72,10 +72,10 @@ export function AutomationPanel({ rules, workflows, companyId, onRefresh }: { ru
const t = useT(); const [activeSection, setActiveSection] = useState<"rules"|"workflows">("rules")
return (
<div className="space-y-6">
<div className="rounded-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="rounded-lg border border-primary/20 bg-gradient-to-br from-primary/5 to-primary/[0.02] px-5 py-4"><div className="flex items-start gap-3"><div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"><Robot size={20} weight="fill" /></div><div><div className="text-[13px] font-semibold text-foreground">{t("auto.automationEngine")}</div><div className="mt-0.5 text-[11px] leading-relaxed text-muted-foreground">Rules trigger actions based on company events. Workflows chain multiple steps into pipelines. Together, they let your AI company run fully autonomously.</div></div></div></div>
<div className="flex gap-1 rounded-lg bg-muted p-1">
<button type="button" onClick={() => setActiveSection("rules")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "rules" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}><Lightning size={15} weight={activeSection === "rules" ? "fill" : "regular"} style={activeSection === "rules" ? { color: ACCENT } : undefined} />Automation Rules ({rules.length})</button>
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} style={activeSection === "workflows" ? { color: ACCENT } : undefined} />Workflows ({workflows.length})</button>
<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"} className={activeSection === "rules" ? "text-primary" : ""} />Automation Rules ({rules.length})</button>
<button type="button" onClick={() => setActiveSection("workflows")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-[13px] font-medium transition ${activeSection === "workflows" ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`}><ArrowsClockwise size={15} weight={activeSection === "workflows" ? "fill" : "regular"} className={activeSection === "workflows" ? "text-primary" : ""} />Workflows ({workflows.length})</button>
</div>
<AnimatePresence mode="wait"><motion.div key={activeSection} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}>{activeSection === "rules" ? <RulesPanel rules={rules} companyId={companyId} onRefresh={onRefresh} /> : <WorkflowsPanel workflows={workflows} companyId={companyId} onRefresh={onRefresh} />}</motion.div></AnimatePresence>
</div>
@@ -2,16 +2,16 @@ import { motion } from "framer-motion"
import { ArrowRight, CaretRight, Gear, Lightning, Pause, Play, Trash } from "@phosphor-icons/react"
import type { AutomationRuleRecord } from "@shared/types"
import { timeAgo } from "../../lib/formatters"
import { ACCENT, LINE, triggerConfig, actionConfig, statusColors } from "./automation-config"
import { triggerConfig, actionConfig, statusColors } from "./automation-config"
export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: AutomationRuleRecord; onToggle: () => void; onDelete: () => void; onEdit: () => void }) {
const triggerInfo = triggerConfig[rule.trigger]; const actionInfo = actionConfig[rule.action]
const TriggerIcon = triggerInfo?.icon ?? Lightning; const colors = statusColors[rule.status]
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-lg border bg-card 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 border-border bg-card transition duration-150 hover:shadow-md hover:-translate-y-px">
<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-lg" 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 bg-primary/10 text-primary"><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={onEdit} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2">
<span className="truncate text-[13px] font-bold text-foreground">{rule.name}</span>
@@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "framer-motion"
import { ArrowsClockwise, CaretRight, Gear, Lightning, Play, Trash } from "@phosphor-icons/react"
import type { WorkflowPipelineRecord, WorkflowStep } from "@shared/types"
import { timeAgo } from "../../lib/formatters"
import { ACCENT, LINE, triggerConfig, actionConfig, workflowStatusColors, stepStatusConfig, safeJsonParse } from "./automation-config"
import { triggerConfig, actionConfig, workflowStatusColors, stepStatusConfig, safeJsonParse } from "./automation-config"
function StepTimeline({ steps }: { steps: WorkflowStep[] }) {
if (steps.length === 0) return <div className="py-4 text-center text-[11px] text-muted-foreground">No steps defined</div>
@@ -16,7 +16,7 @@ function StepTimeline({ steps }: { steps: WorkflowStep[] }) {
return (
<div key={step.id} className="relative flex items-start gap-3">
<div className="absolute -left-5 top-1.5 z-10 h-[10px] w-[10px] rounded-full border-2" style={{ backgroundColor: colors.bg, borderColor: colors.border }} />
<div className="flex-1 rounded-lg 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 ${step.status === "running" ? "border-primary" : "border-border"}`}>
<div className="flex items-center justify-between"><div className="flex items-center gap-2"><span className="text-[11px] font-bold" style={{ color: colors.text }}>{idx + 1}</span><span className="text-[12px] font-medium text-foreground">{step.name}</span></div><span className="rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}>{step.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground"><span>{info?.label ?? step.action}</span>{step.assigneeDepartment ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{step.assigneeDepartment}</span></> : null}{step.requiresApproval ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span className="text-warning">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>
@@ -34,9 +34,9 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
const steps: WorkflowStep[] = useMemo(() => { const parsed = safeJsonParse(workflow.stepsJson); return Array.isArray(parsed) ? (parsed as WorkflowStep[]) : [] }, [workflow.stepsJson])
return (
<motion.div layout initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, x: 20 }} className="group rounded-lg border bg-card 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 border-border bg-card transition hover:shadow-sm">
<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-lg" 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 bg-primary/10 text-primary"><TriggerIcon size={20} weight="fill" /></div>
<button type="button" onClick={() => setExpanded((p) => !p)} className="min-w-0 flex-1 text-left">
<div className="flex items-center gap-2"><span className="truncate text-[13px] font-semibold text-foreground">{workflow.name}</span><span className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.1em]" style={{ backgroundColor: colors.bg, color: colors.text }}><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: colors.dot }} />{workflow.status}</span></div>
<div className="mt-1 flex items-center gap-2 text-[11px] text-muted-foreground"><span className="inline-flex items-center gap-1 rounded-[4px] bg-muted px-1.5 py-0.5 font-medium text-muted-foreground"><Lightning size={10} weight="fill" />{triggerInfo?.label ?? workflow.triggerType}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{steps.length} step{steps.length !== 1 ? "s" : ""}</span><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>{workflow.runCount} run{workflow.runCount !== 1 ? "s" : ""}</span>{workflow.lastRunAt ? <><span className="h-0.5 w-0.5 rounded-full bg-[color:var(--muted-foreground)]" /><span>Last run {timeAgo(workflow.lastRunAt)}</span></> : null}</div>
@@ -49,7 +49,7 @@ function WorkflowCard({ workflow, isRunning, onDelete, onEdit, onRun }: { workfl
</div>
</div>
<AnimatePresence>
{expanded ? <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }} className="overflow-hidden"><div className="border-t px-4 py-4" style={{ borderColor: LINE }}>{workflow.description ? <p className="mb-3 text-[12px] leading-relaxed text-muted-foreground">{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 border-border px-4 py-4">{workflow.description ? <p className="mb-3 text-[12px] leading-relaxed text-muted-foreground">{workflow.description}</p> : null}<StepTimeline steps={steps} /></div></motion.div> : null}
</AnimatePresence>
</motion.div>
)
@@ -2,12 +2,6 @@ import type { AutomationAction, AutomationStatus, AutomationTrigger, WorkflowSta
import { ArrowsClockwise, Eye, Funnel, Lightning, Pause, Play, Plus, Robot, Timer, Trash } from "@phosphor-icons/react"
import { DEPARTMENT_OPTIONS } from "@shared/constants"
// ACCENT/LINE used in template literals for opacity suffixes like `${ACCENT}15`
// Must be hex values (not CSS vars) so hex alpha notation works.
// These are the primary/border colors from the OKLCH design tokens converted to hex.
export const ACCENT = "#343434"
export const LINE = "#ebebeb"
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_created: { label: "Task Created", description: "When a new task is added to the system", icon: Plus },
@@ -63,7 +57,7 @@ export const workflowStatusColors: Record<WorkflowStatus, { bg: string; text: st
export const stepStatusConfig: Record<WorkflowStep["status"], { bg: string; border: string; text: string }> = {
pending: { bg: "var(--muted-foreground)", border: "var(--border)", text: "var(--muted-foreground)" },
running: { bg: "var(--primary)", border: ACCENT, text: ACCENT },
running: { bg: "var(--primary)", border: "var(--primary)", text: "var(--primary)" },
completed: { bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" },
failed: { bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" },
skipped: { bg: "var(--muted-foreground)", border: "var(--border)", text: "var(--muted-foreground)" },