Files
Codewalkers/apps/web/src/components/InitiativeCard.tsx
Lukas May 4e04863e32 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
2026-03-04 07:52:22 +01:00

148 lines
5.0 KiB
TypeScript

import { MoreHorizontal } from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { StatusDot, type StatusVariant } from "@/components/StatusDot";
import { ProgressBar } from "@/components/ProgressBar";
import { trpc } from "@/lib/trpc";
/** Initiative shape as returned by tRPC (Date serialized to string over JSON) */
export interface SerializedInitiative {
id: string;
name: string;
status: "active" | "completed" | "archived";
mergeRequiresApproval: boolean;
branch: string | null;
createdAt: string;
updatedAt: string;
activity: {
state: string;
activePhase?: { id: string; name: string };
phasesTotal: number;
phasesCompleted: number;
};
}
function activityVisual(state: string): { label: string; variant: StatusVariant; pulse: boolean } {
switch (state) {
case "executing": return { label: "Executing", variant: "active", pulse: true };
case "pending_review": return { label: "Pending Review", variant: "warning", pulse: true };
case "discussing": return { label: "Discussing", variant: "active", pulse: true };
case "detailing": return { label: "Detailing", variant: "active", pulse: true };
case "refining": return { label: "Refining", variant: "active", pulse: true };
case "ready": return { label: "Ready", variant: "active", pulse: false };
case "blocked": return { label: "Blocked", variant: "error", pulse: false };
case "complete": return { label: "Complete", variant: "success", pulse: false };
case "planning": return { label: "Planning", variant: "neutral", pulse: false };
case "archived": return { label: "Archived", variant: "neutral", pulse: false };
default: return { label: "Idle", variant: "neutral", pulse: false };
}
}
interface InitiativeCardProps {
initiative: SerializedInitiative;
onClick: () => void;
}
export function InitiativeCard({ initiative, onClick }: InitiativeCardProps) {
const utils = trpc.useUtils();
const archiveMutation = trpc.updateInitiative.useMutation({
onSuccess: () => utils.listInitiatives.invalidate(),
});
const deleteMutation = trpc.deleteInitiative.useMutation({
onSuccess: () => utils.listInitiatives.invalidate(),
});
function handleArchive(e: React.MouseEvent) {
if (
!e.shiftKey &&
!window.confirm(`Archive "${initiative.name}"?`)
) {
return;
}
archiveMutation.mutate({ id: initiative.id, status: "archived" });
}
function handleDelete(e: React.MouseEvent) {
if (
!e.shiftKey &&
!window.confirm(`Delete "${initiative.name}"? This cannot be undone.`)
) {
return;
}
deleteMutation.mutate({ id: initiative.id });
}
const { activity } = initiative;
const visual = activityVisual(activity.state);
return (
<Card
interactive
className="p-4"
onClick={onClick}
>
{/* Row 1: Name + overflow menu */}
<div className="flex items-center justify-between">
<span className="min-w-0 truncate text-base font-bold">
{initiative.name}
</span>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleArchive}>Archive</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={handleDelete}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Row 2: Activity dot + label + active phase + progress */}
<div className="mt-1.5 flex items-center gap-3">
<StatusDot
status={activity.state}
variant={visual.variant}
size="sm"
pulse={visual.pulse}
label={visual.label}
/>
<span className="text-sm font-medium">{visual.label}</span>
{activity.activePhase && (
<span className="truncate text-sm text-muted-foreground">
{activity.activePhase.name}
</span>
)}
{activity.phasesTotal > 0 && (
<>
<ProgressBar
completed={activity.phasesCompleted}
total={activity.phasesTotal}
className="ml-auto w-24"
/>
<span className="hidden text-xs text-muted-foreground md:inline">
{activity.phasesCompleted}/{activity.phasesTotal}
</span>
</>
)}
</div>
</Card>
);
}