Add org chart SVG connector and work page view toggle

- TeamPage: add SVG connector lines between CEO and subordinate agent cards
- WorkPage: add Board/List view toggle and Filter button matching design topbar
- List view shows all tasks in table format with status/priority/assignee columns
This commit is contained in:
ZacharyZhang-NY committed 2026-05-21 17:31:50 -04:00
1 parent 8101dfdc3c
commit 5be8160c15
2 files changed
+92 -27

No files matched your search

@@ -43,10 +43,11 @@ export function TeamPage({ snapshot, actions }: PageProps) {
</div>
<div className="card" style={{ padding: 32, marginBottom: 28 }}>
<div className="section-label">Org</div>
<div style={{ display: "flex", justifyContent: "center", marginBottom: 20 }}>
<div style={{ display: "flex", justifyContent: "center", marginTop: 18, marginBottom: 12 }}>
{employees[0] && <OrgCard name={employees[0].displayName} role={employees[0].role} human />}
</div>
<div className="grid-4">
{agents.length > 0 && <OrgConnector count={agents.length} />}
<div className="grid-4" style={{ marginTop: 18 }}>
{agents.map((agent) => (
<OrgCard key={agent.id} name={agent.name} role={agent.role} />
))}
@@ -88,6 +89,24 @@ export function TeamPage({ snapshot, actions }: PageProps) {
);
}
function OrgConnector({ count }: { count: number }) {
const positions = Array.from({ length: count }, (_, i) => {
const left = count === 1 ? 50 : ((i / (count - 1)) * 100);
return left;
});
return (
<svg width="100%" height="36" style={{ display: "block" }}>
<line x1="50%" x2="50%" y1="0" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
{count > 1 && (
<line x1={`${positions[0]}%`} x2={`${positions[positions.length - 1]}%`} y1="14" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
)}
{positions.map((pos, i) => (
<line key={i} x1={`${pos}%`} x2={`${pos}%`} y1="14" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
))}
</svg>
);
}
function OrgCard({ name, role, human = false }: { name: string; role: string; human?: boolean }) {
return (
<div className="card" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
@@ -32,9 +32,12 @@ function optionalValue(form: FormData, key: string): string | null {
return text ? text : null;
}
type ViewMode = "board" | "list";
export function WorkPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [open, setOpen] = useState(false);
const [view, setView] = useState<ViewMode>("board");
const tasks = snapshot.tasks.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const projects = snapshot.projects.filter((item) => item.orgId === orgId);
@@ -66,35 +69,78 @@ export function WorkPage({ snapshot, actions }: PageProps) {
<h1 className="page-h1">Work</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{tasks.length} tasks · {agents.length} assignees</p>
</div>
<div style={{ marginLeft: "auto" }}>
<div style={{ marginLeft: "auto", display: "flex", gap: 6, alignItems: "center" }}>
<div style={{ display: "flex", gap: 2, padding: 2, border: "1px solid var(--border)", borderRadius: 8 }}>
<button
type="button"
className="button small"
style={{ borderColor: "transparent", background: view === "board" ? "var(--panel-3)" : "transparent" }}
onClick={() => setView("board")}
>
Board
</button>
<button
type="button"
className="button small"
style={{ borderColor: "transparent", background: view === "list" ? "var(--panel-3)" : "transparent" }}
onClick={() => setView("list")}
>
List
</button>
</div>
<button type="button" className="button"><Icon name="filter" size={13} /> Filter</button>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" /> New task</Button>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
{columns.map((column) => {
const columnTasks = tasks.filter((task) => task.status === column.id);
return (
<div key={column.id} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div className="row" style={{ padding: "4px 6px 8px" }}>
<span style={{ width: 8, height: 8, borderRadius: 2, background: columnColor(column.id) }} />
<span style={{ fontSize: 12.5, fontWeight: 540 }}>{column.label}</span>
<span className="num" style={{ color: "var(--text-3)" }}>{columnTasks.length}</span>
{view === "board" ? (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
{columns.map((column) => {
const columnTasks = tasks.filter((task) => task.status === column.id);
return (
<div key={column.id} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div className="row" style={{ padding: "4px 6px 8px" }}>
<span style={{ width: 8, height: 8, borderRadius: 2, background: columnColor(column.id) }} />
<span style={{ fontSize: 12.5, fontWeight: 540 }}>{column.label}</span>
<span className="num" style={{ color: "var(--text-3)" }}>{columnTasks.length}</span>
</div>
{columnTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
assignee={optionalRecordName(task.assigneeAgentId, agents, (agent) => agent.name, "Agent")}
project={optionalRecordName(task.projectId, projects, (project) => project.name, "Project")}
goal={optionalRecordName(task.goalId, goals, (goal) => goal.title, "Goal")}
workspace={optionalRecordName(task.workspaceId, workspaces, (workspace) => workspace.name, "Workspace")}
onMove={(status) => void actions.run(() => window.agentCompany.updateTaskStatus({ orgId, taskId: task.id, status }))}
/>
))}
</div>
{columnTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
assignee={optionalRecordName(task.assigneeAgentId, agents, (agent) => agent.name, "Agent")}
project={optionalRecordName(task.projectId, projects, (project) => project.name, "Project")}
goal={optionalRecordName(task.goalId, goals, (goal) => goal.title, "Goal")}
workspace={optionalRecordName(task.workspaceId, workspaces, (workspace) => workspace.name, "Workspace")}
onMove={(status) => void actions.run(() => window.agentCompany.updateTaskStatus({ orgId, taskId: task.id, status }))}
/>
))}
</div>
);
})}
</div>
);
})}
</div>
) : (
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-row" style={{ gridTemplateColumns: "3fr 1fr 1fr 1fr", background: "var(--panel-2)", color: "var(--text-3)", fontSize: 11 }}>
<div>Title</div><div>Status</div><div>Priority</div><div>Assignee</div>
</div>
{tasks.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No tasks</div>
) : tasks.map((task) => {
const assigneeName = optionalRecordName(task.assigneeAgentId, agents, (a) => a.name, "Agent");
return (
<div key={task.id} className="table-row" style={{ gridTemplateColumns: "3fr 1fr 1fr 1fr" }}>
<div className="row">
{task.risk === "high" && <span style={{ width: 3, height: 12, background: "var(--red)", borderRadius: 2, flexShrink: 0 }} />}
<span style={{ fontWeight: 480 }}>{task.title}</span>
</div>
<Pill>{task.status.replaceAll("_", " ")}</Pill>
<Pill tone={task.priority === "high" ? "amber" : undefined}>{task.priority}</Pill>
<div>{assigneeName ? <div className="row"><Avatar name={assigneeName} size="sm" bot /><span style={{ color: "var(--text-2)" }}>{assigneeName}</span></div> : <span style={{ color: "var(--text-4)" }}>—</span>}</div>
</div>
);
})}
</div>
)}
<Dialog title="New task" open={open} submitLabel="Create" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Title"><input className="input" name="title" required /></Field>
<Field label="Description"><textarea className="input" name="description" required /></Field>