Create src/renderer/components/ui/icons.ts as the single re-export point
for every Phosphor icon the renderer touches, then rewrite the 95 files
that previously imported directly from "@phosphor-icons/react" to pull
from the relative-path-to-barrel instead. The TitleBar commit earlier
in this series already used ui/icons; this commit extends the pattern
to the rest of the renderer.
Benefits now wired in:
- one location where bundle tree-shaking can be reasoned about,
- typos like Briefcase vs BriefCase show up against a finite list,
- re-theming or swapping icon sets is a one-file edit,
- a lint rule that flags raw @phosphor-icons/react imports can be
added later without mass-rewriting call sites.
The barrel also re-exports the `Icon` type so call sites that type
icon props against the Phosphor component type work unchanged.
Test count stays at 363 passing; typecheck stays at 156 errors.
112 lines
5.4 KiB
TypeScript
112 lines
5.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Briefcase, Robot } from "../ui/icons";
|
|
import type { AgentRecord, RunRecord, TaskRecord } from "@shared/types";
|
|
|
|
interface KanbanProps {
|
|
tasks: TaskRecord[];
|
|
agents: AgentRecord[];
|
|
runs: RunRecord[];
|
|
onSelectTask: (taskId: string) => void;
|
|
}
|
|
|
|
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [
|
|
{ status: "backlog", label: "Backlog", tone: "text-muted-foreground", dotColor: "bg-[color:var(--muted-foreground)]" },
|
|
{ status: "todo", label: "Todo", tone: "text-warning", dotColor: "bg-warning" },
|
|
{ status: "in_progress", label: "In Progress", tone: "text-primary", dotColor: "bg-primary" },
|
|
{ status: "in_review", label: "In Review", tone: "text-warning", dotColor: "bg-warning" },
|
|
{ status: "done", label: "Done", tone: "text-success", dotColor: "bg-success" },
|
|
{ status: "blocked", label: "Blocked", tone: "text-destructive", dotColor: "bg-destructive" },
|
|
];
|
|
|
|
const priorityColors: Record<string, string> = {
|
|
critical: "border-l-[color:var(--destructive)]",
|
|
high: "border-l-[color:var(--destructive)]",
|
|
medium: "border-l-[color:var(--primary)]",
|
|
low: "border-l-[color:var(--muted-foreground)]",
|
|
};
|
|
|
|
export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) {
|
|
const tasksByStatus = useMemo(() => {
|
|
const grouped: Record<string, TaskRecord[]> = {};
|
|
for (const col of KANBAN_COLUMNS) grouped[col.status] = [];
|
|
for (const task of tasks) {
|
|
if (task.status === "cancelled") continue;
|
|
const bucket = grouped[task.status];
|
|
if (bucket) bucket.push(task);
|
|
else grouped.backlog?.push(task);
|
|
}
|
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
for (const col of Object.values(grouped)) {
|
|
col.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
}
|
|
return grouped;
|
|
}, [tasks]);
|
|
|
|
if (tasks.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
|
|
<Briefcase size={32} className="mb-3 text-muted-foreground" />
|
|
<div className="text-[14px] font-medium text-muted-foreground">No tasks yet</div>
|
|
<div className="mt-1 text-[12px] text-muted-foreground">Tasks created by agents will appear here on the board</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
{KANBAN_COLUMNS.map((col) => {
|
|
const colTasks = tasksByStatus[col.status] ?? [];
|
|
return (
|
|
<div key={col.status} className="flex w-[220px] min-w-[160px] shrink-0 flex-col lg:w-[240px]">
|
|
<div className="mb-2 flex items-center gap-2 px-1">
|
|
<span className={`h-2.5 w-2.5 rounded-full ${col.dotColor}`} aria-hidden="true" />
|
|
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
|
|
<span className="ml-auto rounded-full bg-muted px-2 py-0.5 text-[10px] font-bold tabular-nums text-muted-foreground">
|
|
{colTasks.length}
|
|
</span>
|
|
</div>
|
|
<div className="flex flex-1 flex-col gap-1.5 rounded-lg bg-muted p-1.5" style={{ minHeight: 120 }}>
|
|
{colTasks.map((task) => {
|
|
const assignee = agents.find((a) => a.id === task.assigneeAgentId);
|
|
const activeRun = runs.find((r) => r.id === task.activeRunId);
|
|
const isRunning = activeRun?.status === "running";
|
|
return (
|
|
<button
|
|
key={task.id}
|
|
type="button"
|
|
onClick={() => onSelectTask(task.id)}
|
|
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-foreground">{task.title}</div>
|
|
<div className="mt-1.5 flex items-center gap-2">
|
|
{assignee ? (
|
|
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
<Robot size={10} />
|
|
{assignee.name}
|
|
</span>
|
|
) : (
|
|
<span className="text-[10px] italic text-muted-foreground">Unassigned</span>
|
|
)}
|
|
{isRunning ? (
|
|
<span className="flex items-center gap-1 whitespace-nowrap text-[10px] text-success">
|
|
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-success" aria-hidden="true" />
|
|
Running
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
{task.priority === "critical" || task.priority === "high" ? (
|
|
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-destructive" : "text-warning"}`}>
|
|
{task.priority}
|
|
</div>
|
|
) : null}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|