Files
zopu-code/repos/web/components/work-os/work-unit-card.tsx
-Puter cc47007fa9 Slice 1 polish: archive dormant code, merge work-os into primitives, add futures
- Archive dormant apps (daemon, desktop, native, tui) and superseded
  packages/server into repos/ for reference
- Archive 21 dead web components (chat page shell, work-os cluster, strays)
  into repos/web/; keep reused message-rendering primitives in components/chat
- Merge @code/work-os into @code/primitives; work.ts absorbed via ./work
  subpath and barrel export; update all four importers
- Add docs/futures.md tracking audio/video (blocked at Flue + provider),
  web layout cleanup, and message rendering quality
- Repoint dev:zopu script to @code/agents (the live Flue server)
- Note repos/ reference-code convention in AGENTS.md
2026-07-27 16:34:17 +05:30

132 lines
3.9 KiB
TypeScript

import { Badge } from "@code/ui/components/badge";
import {
Activity,
ChevronRight,
FileText,
GitPullRequest,
Layers,
Radio,
TriangleAlert,
} from "lucide-react";
import { formatRelativeTime } from "@/lib/work-os/work-unit-projection";
import type { WorkUnitCard as WorkUnitCardData } from "@/lib/work-os/work-unit-projection";
const STATUS_LABEL: Record<WorkUnitCardData["status"], string> = {
completed: "Completed",
failed: "Failed",
"needs-input": "Needs input",
open: "Ready",
queued: "Queued",
working: "In progress",
};
const STATUS_TONE: Record<WorkUnitCardData["status"], string> = {
completed: "bg-emerald-500/15 text-emerald-400",
failed: "bg-destructive/15 text-destructive",
"needs-input": "bg-amber-500/15 text-amber-400",
open: "bg-muted text-muted-foreground",
queued: "bg-violet-500/15 text-violet-400",
working: "bg-blue-500/15 text-blue-400",
};
interface IndicatorProps {
readonly icon: React.ReactNode;
readonly label: string;
readonly value: number;
readonly tone?: string;
}
const Indicator = ({ icon, label, tone, value }: IndicatorProps) => (
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
<span className={tone}>{icon}</span>
{value}
<span className="sr-only">{label}</span>
</span>
);
interface WorkUnitCardProps {
readonly card: WorkUnitCardData;
readonly selected: boolean;
readonly onSelect: () => void;
}
export const WorkUnitCard = ({
card,
onSelect,
selected,
}: WorkUnitCardProps) => (
<button
className={`w-full cursor-pointer rounded-2xl border p-4 text-left transition-all duration-200 ${
selected
? "border-foreground/20 bg-foreground/5"
: "border-border/40 bg-card/40 hover:border-border/70 hover:bg-card/60"
}`}
onClick={onSelect}
type="button"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-[11px] font-medium text-muted-foreground">
#{card.number}
</p>
<h3 className="mt-0.5 text-sm font-semibold leading-5 tracking-tight">
{card.title}
</h3>
</div>
<Badge
className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE[card.status]}`}
variant="secondary"
>
{STATUS_LABEL[card.status]}
</Badge>
</div>
<p className="mt-2 line-clamp-2 text-xs leading-5 text-muted-foreground">
{card.summary}
</p>
<div className="mt-3 flex flex-wrap items-center gap-3 border-t border-border/30 pt-3">
{card.currentActivity ? (
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
<Activity className="size-3" />
{card.currentActivity}
</span>
) : null}
<Indicator
icon={<Radio className="size-3" />}
label="signals"
value={card.signalCount}
/>
<Indicator
icon={<Layers className="size-3" />}
label="steps"
value={card.stepCount}
/>
<Indicator
icon={<FileText className="size-3" />}
label="artifacts"
value={card.artifactCount}
/>
{card.hasPullRequest ? (
<span className="inline-flex items-center gap-1 text-[11px] text-emerald-400">
<GitPullRequest className="size-3" />
PR
</span>
) : null}
{card.needsInput ? (
<span className="inline-flex items-center gap-1 text-[11px] text-amber-400">
<TriangleAlert className="size-3" />
Input needed
</span>
) : null}
<span className="ml-auto inline-flex items-center gap-1">
<span className="text-[11px] text-muted-foreground/70">
{formatRelativeTime(card.updatedAt)}
</span>
<ChevronRight className="size-3.5 text-muted-foreground/50" />
</span>
</div>
</button>
);