Trim the sidebar to the surfaces that earn their place

Inbox, Knowledge, Meetings, and Sprints leave the navigation and their page
components go with them; the backend services and data they surfaced stay
intact for agents and automation. The secondary rail is now Documents,
Social, and Office.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-06 22:57:52 -04:00
1 parent d528cd6f75
commit a11d2dfa07
6 files changed
-511

No files matched your search

@@ -10,11 +10,7 @@ import { FilesPage } from "../pages/FilesPage";
import { ApprovalsPage } from "../pages/ApprovalsPage";
import { SocialPage } from "../pages/SocialPage";
import { DocumentsPage } from "../pages/DocumentsPage";
import { KnowledgePage } from "../pages/KnowledgePage";
import { InboxPage } from "../pages/InboxPage";
import { RunsPage } from "../pages/RunsPage";
import { MeetingsPage } from "../pages/MeetingsPage";
import { SprintsPage } from "../pages/SprintsPage";
import { SettingsPage } from "../pages/SettingsPage";
import { OfficePage } from "../pages/OfficePage";
@@ -45,11 +41,7 @@ export function App() {
chat: <ChatPage {...pageProps} />,
files: <FilesPage {...pageProps} />,
docs: <DocumentsPage {...pageProps} />,
knowledge: <KnowledgePage {...pageProps} />,
inbox: <InboxPage {...pageProps} />,
runs: <RunsPage {...pageProps} />,
meetings: <MeetingsPage {...pageProps} />,
sprints: <SprintsPage {...pageProps} />,
approvals: <ApprovalsPage {...pageProps} />,
social: <SocialPage {...pageProps} />,
office: <OfficePage {...pageProps} />,
@@ -1,197 +0,0 @@
import { useMemo, useState } from "react";
import { Avatar, Pill, Segmented } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import type { AgentRecord, ApprovalRecord, ChatMessageRecord, TaskRecord, TaskRunRecord } from "../../../shared/domain";
import { TASK_STATUS } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId, formatRelative } from "./types";
type InboxFilter = "all" | "approvals" | "failures" | "blocked" | "messages";
const FILTERS: Array<{ id: InboxFilter; label: string }> = [
{ id: "all", label: "All" },
{ id: "approvals", label: "Approvals" },
{ id: "failures", label: "Failures" },
{ id: "blocked", label: "Blocked" },
{ id: "messages", label: "Messages" },
];
type View = "inbox" | "standup" | "performance";
const VIEWS: Array<{ id: View; label: string }> = [
{ id: "inbox", label: "Inbox" },
{ id: "standup", label: "Standup" },
{ id: "performance", label: "Performance" },
];
interface InboxItem {
id: string;
kind: InboxFilter;
title: string;
detail: string;
agentName: string | null;
timestamp: string;
tone: "amber" | "red" | "violet" | undefined;
}
export function InboxPage({ snapshot }: PageProps) {
const orgId = activeOrgId(snapshot);
const agents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]);
const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
const taskRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]);
const approvals = useMemo(() => snapshot.approvals.filter((a) => a.orgId === orgId), [snapshot.approvals, orgId]);
const messages = useMemo(() => snapshot.chatMessages.filter((m) => m.orgId === orgId), [snapshot.chatMessages, orgId]);
const [filter, setFilter] = useState<InboxFilter>("all");
const [view, setView] = useState<View>("inbox");
const items = useMemo(() => buildInboxItems(approvals, taskRuns, tasks, messages, agents), [approvals, taskRuns, tasks, messages, agents]);
const filtered = filter === "all" ? items : items.filter((item) => item.kind === filter);
return (
<div className="page">
<TopbarActions>
<Segmented value={view} onChange={setView} options={VIEWS} />
</TopbarActions>
<h1 className="page-h1">Inbox</h1>
<p className="page-sub">{items.length} {items.length === 1 ? "item" : "items"}</p>
{view === "inbox" && (
<>
<div style={{ marginBottom: 14 }}><Segmented value={filter} onChange={setFilter} options={FILTERS} /></div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{filtered.map((item) => (
<div key={item.id} className="card" style={{ padding: 12 }}>
<div className="row" style={{ gap: 10 }}>
{item.agentName && <Avatar name={item.agentName} size="sm" bot />}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{item.detail}</div>
</div>
<Pill tone={item.tone}>{humanize(item.kind)}</Pill>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatRelative(item.timestamp)}</span>
</div>
</div>
))}
</div>
</>
)}
{view === "standup" && <StandupView agents={agents} tasks={tasks} taskRuns={taskRuns} />}
{view === "performance" && <PerformanceView agents={agents} tasks={tasks} taskRuns={taskRuns} />}
</div>
);
}
function StandupView({ agents, tasks, taskRuns }: { agents: AgentRecord[]; tasks: TaskRecord[]; taskRuns: TaskRunRecord[] }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{agents.map((agent) => {
const agentTasks = tasks.filter((t) => t.assigneeAgentId === agent.id);
const inProgress = agentTasks.filter((t) => t.status === TASK_STATUS.IN_PROGRESS).length;
const done = agentTasks.filter((t) => t.status === TASK_STATUS.DONE).length;
const agentRuns = taskRuns.filter((r) => r.agentId === agent.id);
const lastRun = agentRuns[agentRuns.length - 1];
return (
<div key={agent.id} className="card" style={{ padding: 14 }}>
<div className="row" style={{ gap: 10, marginBottom: 8 }}>
<Avatar name={agent.name} size="sm" bot />
<span style={{ fontWeight: 540 }}>{agent.name}</span>
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{agent.role} · {agent.department}</span>
</div>
<div className="row" style={{ gap: 12, fontSize: 12 }}>
<span>{inProgress} in progress</span>
<span>{done} done</span>
<span>{agentRuns.length} runs</span>
{lastRun && <span style={{ color: "var(--text-3)" }}>{formatRelative(lastRun.createdAt)}</span>}
</div>
</div>
);
})}
</div>
);
}
function PerformanceView({ agents, tasks, taskRuns }: { agents: AgentRecord[]; tasks: TaskRecord[]; taskRuns: TaskRunRecord[] }) {
const totalTasks = tasks.length;
const doneTasks = tasks.filter((t) => t.status === TASK_STATUS.DONE).length;
const completedRuns = taskRuns.filter((r) => r.state === "completed").length;
const failedRuns = taskRuns.filter((r) => r.state === "failed").length;
const successRate = completedRuns + failedRuns > 0 ? Math.round((completedRuns / (completedRuns + failedRuns)) * 100) : 0;
return (
<div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 12, marginBottom: 20 }}>
<MetricCard label="Tasks completed" value={String(doneTasks)} sub={`/ ${totalTasks}`} />
<MetricCard label="Success rate" value={`${successRate}%`} />
<MetricCard label="Total runs" value={String(taskRuns.length)} />
<MetricCard label="Active agents" value={String(agents.filter((a) => a.lifecycle === "active").length)} />
</div>
<div className="section-label" style={{ marginBottom: 10 }}>Agent throughput</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-row head" style={{ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)" }}>
<div>Agent</div><div>Tasks done</div><div>Runs</div><div>Failures</div>
</div>
{agents.map((agent, i) => {
const agentDone = tasks.filter((t) => t.assigneeAgentId === agent.id && t.status === TASK_STATUS.DONE).length;
const agentRuns = taskRuns.filter((r) => r.agentId === agent.id);
const agentFailed = agentRuns.filter((r) => r.state === "failed").length;
return (
<div key={agent.id} className="table-row" style={{ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr)", borderBottom: i === agents.length - 1 ? "none" : "1px solid var(--border-soft)" }}>
<div className="row" style={{ gap: 8 }}>
<Avatar name={agent.name} size="sm" bot />
<span style={{ fontWeight: 480 }}>{agent.name}</span>
</div>
<div className="num mono">{agentDone}</div>
<div className="num mono">{agentRuns.length}</div>
<div>{agentFailed > 0 ? <Pill tone="red">{agentFailed}</Pill> : <span className="num mono" style={{ color: "var(--text-3)" }}>0</span>}</div>
</div>
);
})}
</div>
</div>
);
}
function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
return (
<div className="card" style={{ padding: 14 }}>
<div className="card-label" style={{ marginBottom: 6 }}>{label}</div>
<div className="row" style={{ alignItems: "baseline", gap: 4 }}>
<span className="num" style={{ fontSize: 18, fontWeight: 600 }}>{value}</span>
{sub && <span style={{ fontSize: 12, color: "var(--text-3)" }}>{sub}</span>}
</div>
</div>
);
}
function buildInboxItems(
approvals: ApprovalRecord[],
taskRuns: TaskRunRecord[],
tasks: TaskRecord[],
messages: ChatMessageRecord[],
agents: AgentRecord[],
): InboxItem[] {
const items: InboxItem[] = [];
for (const a of approvals.filter((a) => a.state === "pending")) {
items.push({ id: `app-${a.id}`, kind: "approvals", title: a.title, detail: a.kind.replace(/_/g, " "), agentName: null, timestamp: a.createdAt, tone: "amber" });
}
for (const r of taskRuns.filter((r) => r.state === "failed")) {
const task = tasks.find((t) => t.id === r.taskId);
const agent = agents.find((a) => a.id === r.agentId);
items.push({ id: `fail-${r.id}`, kind: "failures", title: task?.title ?? r.taskId, detail: r.errorMessage ?? "Run failed", agentName: agent?.name ?? null, timestamp: r.completedAt ?? r.createdAt, tone: "red" });
}
for (const t of tasks.filter((t) => t.status === TASK_STATUS.REVIEW)) {
const agent = agents.find((a) => a.id === t.assigneeAgentId);
items.push({ id: `blk-${t.id}`, kind: "blocked", title: t.title, detail: "Awaiting review", agentName: agent?.name ?? null, timestamp: t.updatedAt, tone: "violet" });
}
const recent = messages.filter((m) => m.senderType === "agent").slice(-10);
for (const m of recent) {
const agent = agents.find((a) => a.id === m.senderId);
const preview = m.bodyMd.length > 60 ? `${m.bodyMd.slice(0, 57)}...` : m.bodyMd;
items.push({ id: `msg-${m.id}`, kind: "messages", title: agent?.name ?? "Agent", detail: preview, agentName: agent?.name ?? null, timestamp: m.createdAt, tone: undefined });
}
items.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
return items;
}
function humanize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
@@ -1,94 +0,0 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { Dialog } from "../shared/Dialog";
import type { PageProps } from "./types";
import { activeOrgId, formatRelative } from "./types";
const CATEGORIES = ["process", "technical", "product", "market", "legal", "financial", "operational", "general"] as const;
const IMPORTANCES = ["critical", "high", "medium", "low"] as const;
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function KnowledgePage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const entries = useMemo(() => snapshot.knowledgeEntries.filter((e) => e.orgId === orgId), [snapshot.knowledgeEntries, orgId]);
const [createOpen, setCreateOpen] = useState(false);
const [filter, setFilter] = useState<string>("all");
const filtered = filter === "all" ? entries : entries.filter((e) => e.category === filter);
const create = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createKnowledgeEntry({
orgId,
topic: text(form, "topic"),
category: text(form, "category"),
content: text(form, "content"),
importance: text(form, "importance"),
sourceType: "manual",
sourceId: null,
})).then((r) => { if (r) setCreateOpen(false); });
};
const remove = (entryId: string) => {
void actions.run(() => window.agentCompany.deleteKnowledgeEntry({ orgId, entryId }));
};
return (
<div className="page">
<TopbarActions>
<select className="button" value={filter} onChange={(e) => setFilter(e.target.value)} aria-label="Filter category">
<option value="all">All categories</option>
{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
<Button tone="primary" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> Add entry</Button>
</TopbarActions>
<h1 className="page-h1">Knowledge Base</h1>
<p className="page-sub">{entries.length} {entries.length === 1 ? "entry" : "entries"}</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))", gap: 12 }}>
{filtered.map((entry) => (
<article key={entry.id} className="card" style={{ padding: 16 }}>
<div className="row" style={{ marginBottom: 8 }}>
<span style={{ fontWeight: 540, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{entry.topic}</span>
<IconButton icon="x" label="Delete" onClick={() => remove(entry.id)} />
</div>
<div className="row" style={{ gap: 6, marginBottom: 10 }}>
<Pill>{entry.category}</Pill>
<Pill tone={importanceTone(entry.importance)}>{entry.importance}</Pill>
</div>
<div style={{ fontSize: 14, color: "var(--text-2)", lineHeight: 1.5, whiteSpace: "pre-wrap", maxHeight: 120, overflow: "hidden" }}>
{entry.content}
</div>
<div style={{ marginTop: 8, fontSize: 12, color: "var(--text-3)" }}>{formatRelative(entry.updatedAt)}</div>
</article>
))}
</div>
<Dialog title="New knowledge entry" open={createOpen} submitLabel="Add" onClose={() => setCreateOpen(false)} onSubmit={create}>
<Field label="Topic"><input className="input" name="topic" required /></Field>
<Field label="Category">
<select className="input" name="category" defaultValue="general">
{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</Field>
<Field label="Importance">
<select className="input" name="importance" defaultValue="medium">
{IMPORTANCES.map((i) => <option key={i} value={i}>{i}</option>)}
</select>
</Field>
<Field label="Content"><textarea className="input" name="content" rows={6} required /></Field>
</Dialog>
</div>
);
}
function importanceTone(importance: string): "red" | "amber" | undefined {
if (importance === "critical") return "red";
if (importance === "high") return "amber";
return undefined;
}
@@ -1,97 +0,0 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { Dialog } from "../shared/Dialog";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
const MEETING_TYPES = ["standup", "all_hands", "sprint_review", "retrospective", "planning", "one_on_one", "ad_hoc"] as const;
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function MeetingsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const meetings = useMemo(() => snapshot.meetings.filter((m) => m.orgId === orgId), [snapshot.meetings, orgId]);
const [createOpen, setCreateOpen] = useState(false);
const create = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const scheduledAt = text(form, "scheduledAt");
void actions.run(() => window.agentCompany.createMeeting({
orgId,
title: text(form, "title"),
meetingType: text(form, "meetingType"),
scheduledAt: scheduledAt || null,
durationMinutes: Number(text(form, "durationMinutes")) || 30,
agenda: text(form, "agenda"),
})).then((r) => { if (r) setCreateOpen(false); });
};
const remove = (meetingId: string) => {
void actions.run(() => window.agentCompany.deleteMeeting({ orgId, meetingId }));
};
const scheduled = meetings.filter((m) => m.status === "scheduled");
const completed = meetings.filter((m) => m.status !== "scheduled");
return (
<div className="page">
<TopbarActions>
<Button tone="primary" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> Schedule</Button>
</TopbarActions>
<h1 className="page-h1">Meetings</h1>
<p className="page-sub">{scheduled.length} upcoming · {completed.length} past</p>
{scheduled.length > 0 && <div className="section-label" style={{ marginBottom: 10 }}>Upcoming</div>}
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 24 }}>
{scheduled.map((m) => (
<MeetingCard key={m.id} meeting={m} onDelete={() => remove(m.id)} />
))}
</div>
{completed.length > 0 && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Past</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{completed.map((m) => (
<MeetingCard key={m.id} meeting={m} onDelete={() => remove(m.id)} />
))}
</div>
</>
)}
<Dialog title="Schedule meeting" open={createOpen} submitLabel="Schedule" onClose={() => setCreateOpen(false)} onSubmit={create}>
<Field label="Title"><input className="input" name="title" required /></Field>
<Field label="Type">
<select className="input" name="meetingType" defaultValue="standup">
{MEETING_TYPES.map((t) => <option key={t} value={t}>{t.replace(/_/g, " ")}</option>)}
</select>
</Field>
<Field label="Scheduled at"><input className="input" name="scheduledAt" type="datetime-local" /></Field>
<Field label="Duration (min)"><input className="input" name="durationMinutes" type="number" defaultValue="30" min="5" /></Field>
<Field label="Agenda"><textarea className="input" name="agenda" rows={3} /></Field>
</Dialog>
</div>
);
}
function MeetingCard({ meeting, onDelete }: { meeting: { id: string; title: string; meetingType: string; status: string; scheduledAt: string | null; durationMinutes: number; agenda: string; createdAt: string }; onDelete: () => void }) {
return (
<div className="card" style={{ padding: 14 }}>
<div className="row" style={{ marginBottom: 6 }}>
<span style={{ fontWeight: 540, flex: 1 }}>{meeting.title}</span>
<IconButton icon="x" label="Delete" onClick={onDelete} />
</div>
<div className="row" style={{ gap: 6, marginBottom: 8 }}>
<Pill>{meeting.meetingType.replace(/_/g, " ")}</Pill>
<Pill tone={meeting.status === "scheduled" ? "amber" : undefined}>{meeting.status}</Pill>
<Pill>{meeting.durationMinutes}m</Pill>
</div>
{meeting.scheduledAt && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{new Date(meeting.scheduledAt).toLocaleString()}</div>}
{meeting.agenda && <div style={{ fontSize: 14, color: "var(--text-2)", marginTop: 6, whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{meeting.agenda}</div>}
</div>
);
}
@@ -1,107 +0,0 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { Dialog } from "../shared/Dialog";
import type { SprintRecord, TaskRecord } from "../../../shared/domain";
import { TASK_STATUS } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function SprintsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const sprints = useMemo(() => snapshot.sprints.filter((s) => s.orgId === orgId), [snapshot.sprints, orgId]);
const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
const [createOpen, setCreateOpen] = useState(false);
const create = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createSprint({
orgId,
name: text(form, "name"),
goal: text(form, "goal"),
startDate: text(form, "startDate"),
endDate: text(form, "endDate"),
})).then((r) => { if (r) setCreateOpen(false); });
};
const remove = (sprintId: string) => {
void actions.run(() => window.agentCompany.deleteSprint({ orgId, sprintId }));
};
return (
<div className="page">
<TopbarActions>
<Button tone="primary" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> New sprint</Button>
</TopbarActions>
<h1 className="page-h1">Sprints</h1>
<p className="page-sub">{sprints.length} {sprints.length === 1 ? "sprint" : "sprints"}</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 12 }}>
{sprints.map((sprint) => (
<SprintCard key={sprint.id} sprint={sprint} tasks={tasks} onDelete={() => remove(sprint.id)} />
))}
</div>
<Dialog title="New sprint" open={createOpen} submitLabel="Create" onClose={() => setCreateOpen(false)} onSubmit={create}>
<Field label="Name"><input className="input" name="name" required /></Field>
<Field label="Goal"><textarea className="input" name="goal" rows={2} required /></Field>
<Field label="Start date"><input className="input" name="startDate" type="date" required /></Field>
<Field label="End date"><input className="input" name="endDate" type="date" required /></Field>
</Dialog>
</div>
);
}
function SprintCard({ sprint, tasks, onDelete }: { sprint: SprintRecord; tasks: TaskRecord[]; onDelete: () => void }) {
const start = new Date(sprint.startDate);
const end = new Date(sprint.endDate);
const now = new Date();
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86400000));
const elapsed = Math.max(0, Math.ceil((now.getTime() - start.getTime()) / 86400000));
const progressPct = Math.min(100, Math.round((elapsed / totalDays) * 100));
const done = tasks.filter((t) => t.status === TASK_STATUS.DONE).length;
const total = tasks.length;
const taskPct = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<article className="card" style={{ padding: 16 }}>
<div className="row" style={{ marginBottom: 8 }}>
<span style={{ fontWeight: 560, flex: 1 }}>{sprint.name}</span>
<IconButton icon="x" label="Delete" onClick={onDelete} />
</div>
<div className="row" style={{ gap: 6, marginBottom: 10 }}>
<Pill tone={sprintStatusTone(sprint.status)}>{sprint.status}</Pill>
<Pill>{sprint.startDate} to {sprint.endDate}</Pill>
</div>
<div style={{ fontSize: 14, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12 }}>{sprint.goal}</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<ProgressBar label="Time" pct={progressPct} />
<ProgressBar label="Tasks" pct={taskPct} />
</div>
</article>
);
}
function ProgressBar({ label, pct }: { label: string; pct: number }) {
return (
<div className="row" style={{ gap: 8, fontSize: 12 }}>
<span style={{ width: 40, color: "var(--text-3)" }}>{label}</span>
<div style={{ flex: 1, height: 3, background: "var(--panel-3)", borderRadius: 2, overflow: "hidden" }}>
<div style={{ width: `${pct}%`, height: "100%", background: "var(--text-1)" }} />
</div>
<span className="num" style={{ color: "var(--text-3)", width: 30, textAlign: "right" }}>{pct}%</span>
</div>
);
}
function sprintStatusTone(status: string): "amber" | "green" | "violet" | undefined {
if (status === "planned") return undefined;
if (status === "active") return "amber";
if (status === "completed") return "green";
return "violet";
}
@@ -13,11 +13,7 @@ export type RouteId =
| "chat"
| "files"
| "docs"
| "knowledge"
| "inbox"
| "runs"
| "meetings"
| "sprints"
| "approvals"
| "social"
| "office"
@@ -46,11 +42,7 @@ const primaryNav: NavSpec[] = [
/** Less-frequent surfaces, demoted under a divider so the primary set leads. */
const secondaryNav: NavSpec[] = [
{ id: "inbox", label: "Inbox", icon: "inbox" },
{ id: "docs", label: "Documents", icon: "file" },
{ id: "knowledge", label: "Knowledge", icon: "sparkle" },
{ id: "meetings", label: "Meetings", icon: "calendar" },
{ id: "sprints", label: "Sprints", icon: "target" },
{ id: "social", label: "Social", icon: "social" },
{ id: "office", label: "Office", icon: "team" },
];