feat: Polish pass — display font on headings, empty states, card interactivity, panel dividers
- Apply font-display (Plus Jakarta Sans) to all page h1/h2 headings - Wire interactive Card prop on initiative cards and agent cards - Redesign empty states with icons: agents (Users), inbox (Inbox/MessageSquare), right panels (Terminal) - Unify initiative detail tabs to border-b-2 indicator style matching settings - Strengthen two-panel dividers on agents and inbox pages (lg:border-r) - Clean up filter pill badges — replace nested Badge with simpler span - Increase PhaseAccordion spacing for better readability - Fix error state styling in settings to use status tokens - Increase settings/projects section spacing
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { Inbox, RefreshCw } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { MessageCard } from "@/components/MessageCard";
|
import { MessageCard } from "@/components/MessageCard";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface Agent {
|
interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -119,11 +118,11 @@ export function InboxList({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4 lg:border-r lg:border-border/50 lg:pr-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-lg font-semibold">Agent Inbox</h2>
|
<h2 className="text-lg font-semibold font-display tracking-tight">Agent Inbox</h2>
|
||||||
<Badge variant="secondary">{joined.length}</Badge>
|
<Badge variant="secondary">{joined.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={onRefresh}>
|
<Button variant="outline" size="sm" onClick={onRefresh}>
|
||||||
@@ -139,7 +138,7 @@ export function InboxList({
|
|||||||
{filterOptions.map((opt) => (
|
{filterOptions.map((opt) => (
|
||||||
<Button
|
<Button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
variant={filter === opt.value ? "default" : "outline"}
|
variant={filter === opt.value ? "default" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
onClick={() => setFilter(opt.value)}
|
onClick={() => setFilter(opt.value)}
|
||||||
@@ -153,9 +152,9 @@ export function InboxList({
|
|||||||
{sortOptions.map((opt) => (
|
{sortOptions.map((opt) => (
|
||||||
<Button
|
<Button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
variant={sort === opt.value ? "default" : "outline"}
|
variant={sort === opt.value ? "default" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn("h-7 px-2 text-xs")}
|
className="h-7 px-2 text-xs"
|
||||||
onClick={() => setSort(opt.value)}
|
onClick={() => setSort(opt.value)}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -166,13 +165,12 @@ export function InboxList({
|
|||||||
|
|
||||||
{/* Message list or empty state */}
|
{/* Message list or empty state */}
|
||||||
{sorted.length === 0 ? (
|
{sorted.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-2 py-16">
|
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-12 text-center">
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
<Inbox className="h-8 w-8 text-muted-foreground/40" />
|
||||||
No pending messages
|
<div className="space-y-1">
|
||||||
</p>
|
<p className="text-sm font-medium text-muted-foreground">No pending messages</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-xs text-muted-foreground/70">Agents will appear here when they have questions or status updates</p>
|
||||||
Agents will appear here when they have questions or status updates
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ export function InitiativeCard({ initiative, onClick }: InitiativeCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
interactive
|
||||||
|
className="p-4"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{/* Row 1: Name + overflow menu */}
|
{/* Row 1: Name + overflow menu */}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function PhaseAccordion({
|
|||||||
<div className="border-b border-border">
|
<div className="border-b border-border">
|
||||||
{/* Phase header — clickable to toggle */}
|
{/* Phase header — clickable to toggle */}
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-accent/50"
|
className="flex w-full items-center gap-3 px-4 py-3.5 text-left hover:bg-accent/50"
|
||||||
onClick={() => setExpanded((prev) => !prev)}
|
onClick={() => setExpanded((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{/* Expand / collapse chevron */}
|
{/* Expand / collapse chevron */}
|
||||||
@@ -60,7 +60,7 @@ export function PhaseAccordion({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Phase name */}
|
{/* Phase name */}
|
||||||
<span className="min-w-0 flex-1 truncate font-medium">
|
<span className="min-w-0 flex-1 truncate font-display font-medium">
|
||||||
{phase.name}
|
{phase.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -84,18 +84,20 @@ export function PhaseAccordion({
|
|||||||
|
|
||||||
{/* Expanded content editor + task list */}
|
{/* Expanded content editor + task list */}
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="pb-3 pl-10 pr-4">
|
<div className="pb-4 pl-10 pr-4">
|
||||||
<PhaseContentEditor phaseId={phase.id} initiativeId={phase.initiativeId} />
|
<PhaseContentEditor phaseId={phase.id} initiativeId={phase.initiativeId} />
|
||||||
{tasks.map((entry, idx) => (
|
<div className="mt-1 space-y-1">
|
||||||
<TaskRow
|
{tasks.map((entry, idx) => (
|
||||||
key={entry.task.id}
|
<TaskRow
|
||||||
task={entry.task}
|
key={entry.task.id}
|
||||||
agentName={entry.agentName}
|
task={entry.task}
|
||||||
blockedBy={entry.blockedBy}
|
agentName={entry.agentName}
|
||||||
isLast={idx === tasks.length - 1}
|
blockedBy={entry.blockedBy}
|
||||||
onClick={() => onTaskClick(entry.task.id)}
|
isLast={idx === tasks.length - 1}
|
||||||
/>
|
onClick={() => onTaskClick(entry.task.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { AlertCircle, RefreshCw } from "lucide-react";
|
import { AlertCircle, RefreshCw, Terminal, Users } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -11,7 +11,6 @@ import { trpc } from "@/lib/trpc";
|
|||||||
import { AgentOutputViewer } from "@/components/AgentOutputViewer";
|
import { AgentOutputViewer } from "@/components/AgentOutputViewer";
|
||||||
import { AgentActions } from "@/components/AgentActions";
|
import { AgentActions } from "@/components/AgentActions";
|
||||||
import { formatRelativeTime } from "@/lib/utils";
|
import { formatRelativeTime } from "@/lib/utils";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { modeLabel } from "@/lib/labels";
|
import { modeLabel } from "@/lib/labels";
|
||||||
import { StatusDot } from "@/components/StatusDot";
|
import { StatusDot } from "@/components/StatusDot";
|
||||||
import { useLiveUpdates } from "@/hooks";
|
import { useLiveUpdates } from "@/hooks";
|
||||||
@@ -193,7 +192,7 @@ function AgentsPage() {
|
|||||||
<div className="shrink-0 space-y-3">
|
<div className="shrink-0 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-lg font-semibold">Agents</h1>
|
<h1 className="font-display text-lg font-semibold tracking-tight">Agents</h1>
|
||||||
<Badge variant="secondary">{agents.length}</Badge>
|
<Badge variant="secondary">{agents.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||||
@@ -216,12 +215,9 @@ function AgentsPage() {
|
|||||||
onClick={() => navigate({ to: "/agents", search: { filter: opt.value } })}
|
onClick={() => navigate({ to: "/agents", search: { filter: opt.value } })}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
<Badge
|
<span className="ml-1.5 text-[10px] tabular-nums opacity-70">
|
||||||
variant="secondary"
|
|
||||||
className="ml-1.5 h-4 min-w-4 px-1 text-[10px]"
|
|
||||||
>
|
|
||||||
{counts[opt.value]}
|
{counts[opt.value]}
|
||||||
</Badge>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -235,12 +231,14 @@ function AgentsPage() {
|
|||||||
className="grid grid-cols-1 gap-4 lg:grid-cols-[320px_1fr] min-h-0 flex-1"
|
className="grid grid-cols-1 gap-4 lg:grid-cols-[320px_1fr] min-h-0 flex-1"
|
||||||
>
|
>
|
||||||
{/* Left: Agent List */}
|
{/* Left: Agent List */}
|
||||||
<div className="overflow-y-auto min-h-0 space-y-2">
|
<div className="overflow-y-auto min-h-0 space-y-2 lg:border-r lg:border-border/50 lg:pr-4">
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed p-8 text-center">
|
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-12 text-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<Users className="h-8 w-8 text-muted-foreground/40" />
|
||||||
No agents match this filter
|
<div className="space-y-1">
|
||||||
</p>
|
<p className="text-sm font-medium text-muted-foreground">No agents match this filter</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70">Try selecting a different filter above</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filtered.map((agent, i) => (
|
filtered.map((agent, i) => (
|
||||||
@@ -255,10 +253,9 @@ function AgentsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
interactive
|
||||||
"cursor-pointer p-3 transition-colors hover:bg-muted/50",
|
selected={selectedAgentId === agent.id}
|
||||||
selectedAgentId === agent.id && "bg-muted"
|
className="p-3"
|
||||||
)}
|
|
||||||
onClick={() => setSelectedAgentId(agent.id)}
|
onClick={() => setSelectedAgentId(agent.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@@ -321,10 +318,12 @@ function AgentsPage() {
|
|||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center rounded-lg border border-dashed">
|
<div className="flex h-full flex-col items-center justify-center gap-3 rounded-lg border border-dashed">
|
||||||
<p className="text-sm text-muted-foreground">
|
<Terminal className="h-10 w-10 text-muted-foreground/30" />
|
||||||
Select an agent to view output
|
<div className="space-y-1 text-center">
|
||||||
</p>
|
<p className="text-sm font-medium text-muted-foreground">No agent selected</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70">Select an agent from the list to view output</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle, MessageSquare } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/Skeleton";
|
import { Skeleton } from "@/components/Skeleton";
|
||||||
@@ -255,10 +255,12 @@ function InboxPage() {
|
|||||||
|
|
||||||
{/* Empty detail panel placeholder */}
|
{/* Empty detail panel placeholder */}
|
||||||
{!selectedAgent && (
|
{!selectedAgent && (
|
||||||
<div className="hidden items-center justify-center rounded-lg border border-dashed border-border p-8 lg:flex">
|
<div className="hidden flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border p-8 lg:flex">
|
||||||
<p className="text-sm text-muted-foreground">
|
<MessageSquare className="h-10 w-10 text-muted-foreground/30" />
|
||||||
Select an agent to view details
|
<div className="space-y-1 text-center">
|
||||||
</p>
|
<p className="text-sm font-medium text-muted-foreground">No message selected</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70">Select an agent from the inbox to view details</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AlertCircle } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/Skeleton";
|
import { Skeleton } from "@/components/Skeleton";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { InitiativeHeader } from "@/components/InitiativeHeader";
|
import { InitiativeHeader } from "@/components/InitiativeHeader";
|
||||||
import { ContentTab } from "@/components/editor/ContentTab";
|
import { ContentTab } from "@/components/editor/ContentTab";
|
||||||
import { ExecutionTab } from "@/components/ExecutionTab";
|
import { ExecutionTab } from "@/components/ExecutionTab";
|
||||||
@@ -114,7 +115,7 @@ function InitiativeDetailPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
<motion.div
|
<motion.nav
|
||||||
initial={{ opacity: 0, y: 8 }}
|
initial={{ opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3, delay: 0.05, ease: [0, 0, 0.2, 1] }}
|
transition={{ duration: 0.3, delay: 0.05, ease: [0, 0, 0.2, 1] }}
|
||||||
@@ -129,16 +130,17 @@ function InitiativeDetailPage() {
|
|||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={`px-4 py-2 text-sm font-medium capitalize border-b-2 transition-colors ${
|
className={cn(
|
||||||
|
"px-4 py-2 text-sm font-medium border-b-2 transition-colors",
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? "border-primary text-foreground"
|
? "border-primary text-foreground"
|
||||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.nav>
|
||||||
|
|
||||||
{/* Tab content */}
|
{/* Tab content */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function DashboardPage() {
|
|||||||
>
|
>
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold">Initiatives</h1>
|
<h1 className="font-display text-2xl font-bold tracking-tight">Initiatives</h1>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
|
|||||||
Reference in New Issue
Block a user