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:
Lukas May
2026-03-04 07:52:22 +01:00
parent 2b62160c95
commit 4e04863e32
7 changed files with 64 additions and 60 deletions

View File

@@ -1,9 +1,8 @@
import { useMemo, useState } from "react";
import { RefreshCw } from "lucide-react";
import { Inbox, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { MessageCard } from "@/components/MessageCard";
import { cn } from "@/lib/utils";
interface Agent {
id: string;
@@ -119,11 +118,11 @@ export function InboxList({
];
return (
<div className="space-y-4">
<div className="space-y-4 lg:border-r lg:border-border/50 lg:pr-4">
{/* Header */}
<div className="flex items-center justify-between">
<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>
</div>
<Button variant="outline" size="sm" onClick={onRefresh}>
@@ -139,7 +138,7 @@ export function InboxList({
{filterOptions.map((opt) => (
<Button
key={opt.value}
variant={filter === opt.value ? "default" : "outline"}
variant={filter === opt.value ? "default" : "ghost"}
size="sm"
className="h-7 px-2 text-xs"
onClick={() => setFilter(opt.value)}
@@ -153,9 +152,9 @@ export function InboxList({
{sortOptions.map((opt) => (
<Button
key={opt.value}
variant={sort === opt.value ? "default" : "outline"}
variant={sort === opt.value ? "default" : "ghost"}
size="sm"
className={cn("h-7 px-2 text-xs")}
className="h-7 px-2 text-xs"
onClick={() => setSort(opt.value)}
>
{opt.label}
@@ -166,13 +165,12 @@ export function InboxList({
{/* Message list or empty state */}
{sorted.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-16">
<p className="text-lg font-medium text-muted-foreground">
No pending messages
</p>
<p className="text-sm text-muted-foreground">
Agents will appear here when they have questions or status updates
</p>
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-12 text-center">
<Inbox className="h-8 w-8 text-muted-foreground/40" />
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">No pending messages</p>
<p className="text-xs text-muted-foreground/70">Agents will appear here when they have questions or status updates</p>
</div>
</div>
) : (
<div className="space-y-2">

View File

@@ -84,7 +84,8 @@ export function InitiativeCard({ initiative, onClick }: InitiativeCardProps) {
return (
<Card
className="cursor-pointer p-4 transition-colors hover:bg-accent/50"
interactive
className="p-4"
onClick={onClick}
>
{/* Row 1: Name + overflow menu */}

View File

@@ -49,7 +49,7 @@ export function PhaseAccordion({
<div className="border-b border-border">
{/* Phase header — clickable to toggle */}
<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)}
>
{/* Expand / collapse chevron */}
@@ -60,7 +60,7 @@ export function PhaseAccordion({
)}
{/* 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}
</span>
@@ -84,18 +84,20 @@ export function PhaseAccordion({
{/* Expanded content editor + task list */}
{expanded && (
<div className="pb-3 pl-10 pr-4">
<div className="pb-4 pl-10 pr-4">
<PhaseContentEditor phaseId={phase.id} initiativeId={phase.initiativeId} />
{tasks.map((entry, idx) => (
<TaskRow
key={entry.task.id}
task={entry.task}
agentName={entry.agentName}
blockedBy={entry.blockedBy}
isLast={idx === tasks.length - 1}
onClick={() => onTaskClick(entry.task.id)}
/>
))}
<div className="mt-1 space-y-1">
{tasks.map((entry, idx) => (
<TaskRow
key={entry.task.id}
task={entry.task}
agentName={entry.agentName}
blockedBy={entry.blockedBy}
isLast={idx === tasks.length - 1}
onClick={() => onTaskClick(entry.task.id)}
/>
))}
</div>
</div>
)}
</div>

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
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 { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
@@ -11,7 +11,6 @@ import { trpc } from "@/lib/trpc";
import { AgentOutputViewer } from "@/components/AgentOutputViewer";
import { AgentActions } from "@/components/AgentActions";
import { formatRelativeTime } from "@/lib/utils";
import { cn } from "@/lib/utils";
import { modeLabel } from "@/lib/labels";
import { StatusDot } from "@/components/StatusDot";
import { useLiveUpdates } from "@/hooks";
@@ -193,7 +192,7 @@ function AgentsPage() {
<div className="shrink-0 space-y-3">
<div className="flex items-center justify-between">
<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>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh}>
@@ -216,12 +215,9 @@ function AgentsPage() {
onClick={() => navigate({ to: "/agents", search: { filter: opt.value } })}
>
{opt.label}
<Badge
variant="secondary"
className="ml-1.5 h-4 min-w-4 px-1 text-[10px]"
>
<span className="ml-1.5 text-[10px] tabular-nums opacity-70">
{counts[opt.value]}
</Badge>
</span>
</Button>
))}
</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"
>
{/* 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 ? (
<div className="rounded-lg border border-dashed p-8 text-center">
<p className="text-sm text-muted-foreground">
No agents match this filter
</p>
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-12 text-center">
<Users className="h-8 w-8 text-muted-foreground/40" />
<div className="space-y-1">
<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>
) : (
filtered.map((agent, i) => (
@@ -255,10 +253,9 @@ function AgentsPage() {
}}
>
<Card
className={cn(
"cursor-pointer p-3 transition-colors hover:bg-muted/50",
selectedAgentId === agent.id && "bg-muted"
)}
interactive
selected={selectedAgentId === agent.id}
className="p-3"
onClick={() => setSelectedAgentId(agent.id)}
>
<div className="flex items-center justify-between gap-2">
@@ -321,10 +318,12 @@ function AgentsPage() {
onStop={handleStop}
/>
) : (
<div className="flex h-full items-center justify-center rounded-lg border border-dashed">
<p className="text-sm text-muted-foreground">
Select an agent to view output
</p>
<div className="flex h-full flex-col items-center justify-center gap-3 rounded-lg border border-dashed">
<Terminal className="h-10 w-10 text-muted-foreground/30" />
<div className="space-y-1 text-center">
<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>

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { motion } from "motion/react";
import { AlertCircle } from "lucide-react";
import { AlertCircle, MessageSquare } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Skeleton } from "@/components/Skeleton";
@@ -255,10 +255,12 @@ function InboxPage() {
{/* Empty detail panel placeholder */}
{!selectedAgent && (
<div className="hidden items-center justify-center rounded-lg border border-dashed border-border p-8 lg:flex">
<p className="text-sm text-muted-foreground">
Select an agent to view details
</p>
<div className="hidden flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border p-8 lg:flex">
<MessageSquare className="h-10 w-10 text-muted-foreground/30" />
<div className="space-y-1 text-center">
<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>

View File

@@ -4,6 +4,7 @@ import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/Skeleton";
import { trpc } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import { InitiativeHeader } from "@/components/InitiativeHeader";
import { ContentTab } from "@/components/editor/ContentTab";
import { ExecutionTab } from "@/components/ExecutionTab";
@@ -114,7 +115,7 @@ function InitiativeDetailPage() {
/>
{/* Tab bar */}
<motion.div
<motion.nav
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.05, ease: [0, 0, 0.2, 1] }}
@@ -129,16 +130,17 @@ function InitiativeDetailPage() {
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
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
)}
>
{tab}
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</motion.div>
</motion.nav>
{/* Tab content */}
<motion.div

View File

@@ -40,7 +40,7 @@ function DashboardPage() {
>
{/* Page header */}
<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">
<select
value={statusFilter}