From d912670f72514436a37c7c12545f9f585d772a3a Mon Sep 17 00:00:00 2001 From: Forgotten Date: Tue, 17 Feb 2026 10:53:20 -0600 Subject: [PATCH] Polish UI: enhance dialogs, command palette, and page layouts Expand NewIssueDialog with richer form fields. Add NewProjectDialog. Enhance CommandPalette with more actions and search. Improve CompanySwitcher, EmptyState, and IssueProperties. Flesh out Activity, Companies, Dashboard, and Inbox pages with real content and layouts. Refine sidebar, routing, and dialog context. CSS tweaks for dark theme. Co-Authored-By: Claude Opus 4.6 --- ui/src/App.tsx | 7 +- ui/src/components/CommandPalette.tsx | 130 ++++++-- ui/src/components/CompanySwitcher.tsx | 34 ++- ui/src/components/EmptyState.tsx | 8 +- ui/src/components/IssueProperties.tsx | 47 ++- ui/src/components/Layout.tsx | 10 +- ui/src/components/NewIssueDialog.tsx | 393 +++++++++++++++++++------ ui/src/components/NewProjectDialog.tsx | 251 ++++++++++++++++ ui/src/components/Sidebar.tsx | 20 +- ui/src/components/ui/dialog.tsx | 4 +- ui/src/context/DialogContext.tsx | 24 +- ui/src/index.css | 5 + ui/src/pages/Activity.tsx | 138 +++++++-- ui/src/pages/Agents.tsx | 18 +- ui/src/pages/Companies.tsx | 100 ++++++- ui/src/pages/Dashboard.tsx | 135 +++++++-- ui/src/pages/Goals.tsx | 7 +- ui/src/pages/Inbox.tsx | 174 +++++++++-- ui/src/pages/IssueDetail.tsx | 2 +- ui/src/pages/Issues.tsx | 20 +- ui/src/pages/Org.tsx | 8 +- ui/src/pages/Projects.tsx | 20 +- 22 files changed, 1301 insertions(+), 254 deletions(-) create mode 100644 ui/src/components/NewProjectDialog.tsx diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3e644766..52d68289 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import { Layout } from "./components/Layout"; import { Dashboard } from "./pages/Dashboard"; import { Companies } from "./pages/Companies"; @@ -21,14 +21,15 @@ export function App() { return ( }> - } /> + } /> + } /> } /> } /> } /> } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index da967004..a26da594 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { useCompany } from "../context/CompanyContext"; +import { useDialog } from "../context/DialogContext"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; @@ -11,8 +12,21 @@ import { CommandInput, CommandItem, CommandList, + CommandSeparator, } from "@/components/ui/command"; -import { CircleDot, Bot, Hexagon, Target, LayoutDashboard, Inbox } from "lucide-react"; +import { + CircleDot, + Bot, + Hexagon, + Target, + LayoutDashboard, + Inbox, + DollarSign, + History, + GitBranch, + SquarePen, + Plus, +} from "lucide-react"; import type { Issue, Agent, Project } from "@paperclip/shared"; export function CommandPalette() { @@ -22,6 +36,7 @@ export function CommandPalette() { const [projects, setProjects] = useState([]); const navigate = useNavigate(); const { selectedCompanyId } = useCompany(); + const { openNewIssue } = useDialog(); useEffect(() => { function handleKeyDown(e: KeyboardEvent) { @@ -57,6 +72,11 @@ export function CommandPalette() { navigate(path); } + const agentName = (id: string | null) => { + if (!id) return null; + return agents.find((a) => a.id === id)?.name ?? null; + }; + return ( @@ -64,7 +84,7 @@ export function CommandPalette() { No results found. - go("/")}> + go("/dashboard")}> Dashboard @@ -72,7 +92,7 @@ export function CommandPalette() { Inbox - go("/tasks")}> + go("/issues")}> Issues @@ -88,42 +108,92 @@ export function CommandPalette() { Agents + go("/costs")}> + + Costs + + go("/activity")}> + + Activity + + go("/org")}> + + Org Chart + + + + + + + { + setOpen(false); + openNewIssue(); + }} + > + + Create new issue + C + + go("/agents")}> + + Create new agent + + go("/projects")}> + + Create new project + {issues.length > 0 && ( - - {issues.slice(0, 10).map((issue) => ( - go(`/issues/${issue.id}`)}> - - - {issue.id.slice(0, 8)} - - {issue.title} - - ))} - + <> + + + {issues.slice(0, 10).map((issue) => ( + go(`/issues/${issue.id}`)}> + + + {issue.id.slice(0, 8)} + + {issue.title} + {issue.assigneeAgentId && ( + + {agentName(issue.assigneeAgentId)} + + )} + + ))} + + )} {agents.length > 0 && ( - - {agents.slice(0, 10).map((agent) => ( - go(`/agents/${agent.id}`)}> - - {agent.name} - - ))} - + <> + + + {agents.slice(0, 10).map((agent) => ( + go(`/agents/${agent.id}`)}> + + {agent.name} + {agent.role} + + ))} + + )} {projects.length > 0 && ( - - {projects.slice(0, 10).map((project) => ( - go(`/projects/${project.id}`)}> - - {project.name} - - ))} - + <> + + + {projects.slice(0, 10).map((project) => ( + go(`/projects/${project.id}`)}> + + {project.name} + + ))} + + )} diff --git a/ui/src/components/CompanySwitcher.tsx b/ui/src/components/CompanySwitcher.tsx index 2e25c23a..77e01904 100644 --- a/ui/src/components/CompanySwitcher.tsx +++ b/ui/src/components/CompanySwitcher.tsx @@ -1,4 +1,5 @@ import { ChevronsUpDown, Plus } from "lucide-react"; +import { useNavigate } from "react-router-dom"; import { useCompany } from "../context/CompanyContext"; import { DropdownMenu, @@ -10,23 +11,39 @@ import { } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; +function statusDotColor(status?: string): string { + switch (status) { + case "active": + return "bg-green-400"; + case "paused": + return "bg-yellow-400"; + case "archived": + return "bg-neutral-400"; + default: + return "bg-green-400"; + } +} + export function CompanySwitcher() { const { companies, selectedCompany, setSelectedCompanyId } = useCompany(); + const navigate = useNavigate(); return ( @@ -38,6 +55,7 @@ export function CompanySwitcher() { onClick={() => setSelectedCompanyId(company.id)} className={company.id === selectedCompany?.id ? "bg-accent" : ""} > + {company.name} ))} @@ -45,11 +63,9 @@ export function CompanySwitcher() { No companies )} - - - - Manage Companies - + navigate("/companies")}> + + Manage Companies diff --git a/ui/src/components/EmptyState.tsx b/ui/src/components/EmptyState.tsx index b795da5f..8b6091f2 100644 --- a/ui/src/components/EmptyState.tsx +++ b/ui/src/components/EmptyState.tsx @@ -1,3 +1,4 @@ +import { Plus } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -11,10 +12,13 @@ interface EmptyStateProps { export function EmptyState({ icon: Icon, message, action, onAction }: EmptyStateProps) { return (
- +
+ +

{message}

{action && onAction && ( - )} diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index f847d284..69f37ec9 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -1,7 +1,10 @@ import type { Issue } from "@paperclip/shared"; +import { useCompany } from "../context/CompanyContext"; +import { useAgents } from "../hooks/useAgents"; import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; import { formatDate } from "../lib/utils"; +import { timeAgo } from "../lib/timeAgo"; import { Separator } from "@/components/ui/separator"; interface IssuePropertiesProps { @@ -27,6 +30,15 @@ function priorityLabel(priority: string): string { } export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) { + const { selectedCompanyId } = useCompany(); + const { data: agents } = useAgents(selectedCompanyId); + + const agentName = (id: string | null) => { + if (!id || !agents) return null; + const agent = agents.find((a) => a.id === id); + return agent?.name ?? id.slice(0, 8); + }; + return (
@@ -46,30 +58,37 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) { {priorityLabel(issue.priority)} - {issue.assigneeAgentId && ( - - {issue.assigneeAgentId.slice(0, 8)} - - )} + + + {issue.assigneeAgentId ? agentName(issue.assigneeAgentId) : "Unassigned"} + + - {issue.projectId && ( - - {issue.projectId.slice(0, 8)} - - )} + + + {issue.projectId ? issue.projectId.slice(0, 8) : "None"} + +
- - {issue.id.slice(0, 8)} - + {issue.startedAt && ( + + {formatDate(issue.startedAt)} + + )} + {issue.completedAt && ( + + {formatDate(issue.completedAt)} + + )} {formatDate(issue.createdAt)} - {formatDate(issue.updatedAt)} + {timeAgo(issue.updatedAt)}
diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 40ff59f4..8176c6f0 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -5,6 +5,7 @@ import { BreadcrumbBar } from "./BreadcrumbBar"; import { PropertiesPanel } from "./PropertiesPanel"; import { CommandPalette } from "./CommandPalette"; import { NewIssueDialog } from "./NewIssueDialog"; +import { NewProjectDialog } from "./NewProjectDialog"; import { useDialog } from "../context/DialogContext"; import { usePanel } from "../context/PanelContext"; import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts"; @@ -27,18 +28,18 @@ export function Layout() { }); return ( -
+
-
+
-
+
@@ -47,6 +48,7 @@ export function Layout() {
+
); } diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 21da228c..c7e8bbcf 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -1,25 +1,51 @@ -import { useState } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { issuesApi } from "../api/issues"; +import { projectsApi } from "../api/projects"; +import { useAgents } from "../hooks/useAgents"; +import { useApi } from "../hooks/useApi"; import { Dialog, DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Maximize2, + Minimize2, + MoreHorizontal, + CircleDot, + Minus, + ArrowUp, + ArrowDown, + AlertTriangle, + User, + Hexagon, + Tag, + Calendar, +} from "lucide-react"; +import { cn } from "../lib/utils"; +import type { Project, Agent } from "@paperclip/shared"; + +const statuses = [ + { value: "backlog", label: "Backlog", color: "text-muted-foreground" }, + { value: "todo", label: "Todo", color: "text-blue-400" }, + { value: "in_progress", label: "In Progress", color: "text-yellow-400" }, + { value: "in_review", label: "In Review", color: "text-violet-400" }, + { value: "done", label: "Done", color: "text-green-400" }, +]; + +const priorities = [ + { value: "critical", label: "Critical", icon: AlertTriangle, color: "text-red-400" }, + { value: "high", label: "High", icon: ArrowUp, color: "text-orange-400" }, + { value: "medium", label: "Medium", icon: Minus, color: "text-yellow-400" }, + { value: "low", label: "Low", icon: ArrowDown, color: "text-blue-400" }, +]; interface NewIssueDialogProps { onCreated?: () => void; @@ -27,22 +53,50 @@ interface NewIssueDialogProps { export function NewIssueDialog({ onCreated }: NewIssueDialogProps) { const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog(); - const { selectedCompanyId } = useCompany(); + const { selectedCompanyId, selectedCompany } = useCompany(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); - const [status, setStatus] = useState(newIssueDefaults.status ?? "todo"); - const [priority, setPriority] = useState(newIssueDefaults.priority ?? "medium"); + const [status, setStatus] = useState("todo"); + const [priority, setPriority] = useState(""); + const [assigneeId, setAssigneeId] = useState(""); + const [projectId, setProjectId] = useState(""); + const [expanded, setExpanded] = useState(false); const [submitting, setSubmitting] = useState(false); + // Popover states + const [statusOpen, setStatusOpen] = useState(false); + const [priorityOpen, setPriorityOpen] = useState(false); + const [assigneeOpen, setAssigneeOpen] = useState(false); + const [projectOpen, setProjectOpen] = useState(false); + const [moreOpen, setMoreOpen] = useState(false); + + const { data: agents } = useAgents(selectedCompanyId); + + const projectsFetcher = useCallback(() => { + if (!selectedCompanyId) return Promise.resolve([] as Project[]); + return projectsApi.list(selectedCompanyId); + }, [selectedCompanyId]); + const { data: projects } = useApi(projectsFetcher); + + useEffect(() => { + if (newIssueOpen) { + setStatus(newIssueDefaults.status ?? "todo"); + setPriority(newIssueDefaults.priority ?? ""); + setProjectId(newIssueDefaults.projectId ?? ""); + } + }, [newIssueOpen, newIssueDefaults]); + function reset() { setTitle(""); setDescription(""); setStatus("todo"); - setPriority("medium"); + setPriority(""); + setAssigneeId(""); + setProjectId(""); + setExpanded(false); } - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); + async function handleSubmit() { if (!selectedCompanyId || !title.trim()) return; setSubmitting(true); @@ -51,7 +105,9 @@ export function NewIssueDialog({ onCreated }: NewIssueDialogProps) { title: title.trim(), description: description.trim() || undefined, status, - priority, + priority: priority || "medium", + ...(assigneeId ? { assigneeAgentId: assigneeId } : {}), + ...(projectId ? { projectId } : {}), }); reset(); closeNewIssue(); @@ -61,6 +117,18 @@ export function NewIssueDialog({ onCreated }: NewIssueDialogProps) { } } + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleSubmit(); + } + } + + const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!; + const currentPriority = priorities.find((p) => p.value === priority); + const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId); + const currentProject = (projects ?? []).find((p) => p.id === projectId); + return ( - - - New Issue - -
-
- - setTitle(e.target.value)} - autoFocus - /> + + {/* Header bar */} +
+
+ {selectedCompany && ( + + {selectedCompany.name.slice(0, 3).toUpperCase()} + + )} + + New issue
-
- -