Files
Codewalkers/packages/web/src/components/ChangeSetBanner.tsx
Lukas May 342b490fe7 feat: Task decomposition for Tailwind/Radix/shadcn foundation setup
Decomposed "Foundation Setup - Install Dependencies & Configure Tailwind"
phase into 6 executable tasks:

1. Install Tailwind CSS, PostCSS & Autoprefixer
2. Map MUI theme to Tailwind design tokens
3. Setup CSS variables for dynamic theming
4. Install Radix UI primitives
5. Initialize shadcn/ui and setup component directory
6. Move MUI to devDependencies and verify setup

Tasks follow logical dependency chain with final human verification
checkpoint before proceeding with component migration.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 09:48:51 +01:00

140 lines
4.4 KiB
TypeScript

import { useState, useCallback } from "react";
import { ChevronDown, ChevronRight, Undo2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { trpc } from "@/lib/trpc";
import type { ChangeSet } from "@codewalk-district/shared";
interface ChangeSetBannerProps {
changeSet: ChangeSet;
onDismiss: () => void;
}
const MODE_LABELS: Record<string, string> = {
breakdown: "phases",
decompose: "tasks",
refine: "pages",
};
export function ChangeSetBanner({ changeSet, onDismiss }: ChangeSetBannerProps) {
const [expanded, setExpanded] = useState(false);
const [conflicts, setConflicts] = useState<string[] | null>(null);
const detailQuery = trpc.getChangeSet.useQuery(
{ id: changeSet.id },
{ enabled: expanded },
);
const revertMutation = trpc.revertChangeSet.useMutation({
onSuccess: (result) => {
if (!result.success && "conflicts" in result) {
setConflicts(result.conflicts);
} else {
setConflicts(null);
}
},
});
const handleRevert = useCallback(
(force?: boolean) => {
revertMutation.mutate({ id: changeSet.id, force });
},
[changeSet.id, revertMutation],
);
const entries = detailQuery.data?.entries ?? [];
const entityLabel = MODE_LABELS[changeSet.mode] ?? "entities";
const isReverted = changeSet.status === "reverted";
return (
<div className="rounded-lg border border-border bg-card p-3 space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<button
className="flex items-center gap-1 text-sm font-medium hover:text-foreground/80"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
)}
{changeSet.summary ??
`Agent ${isReverted ? "reverted" : "applied"} ${entityLabel}`}
</button>
{isReverted && (
<span className="text-xs text-muted-foreground italic">
(reverted)
</span>
)}
</div>
<div className="flex gap-1.5 shrink-0">
{!isReverted && (
<Button
variant="outline"
size="sm"
onClick={() => handleRevert()}
disabled={revertMutation.isPending}
className="gap-1"
>
<Undo2 className="h-3 w-3" />
{revertMutation.isPending ? "Reverting..." : "Revert"}
</Button>
)}
<Button variant="ghost" size="sm" onClick={onDismiss}>
Dismiss
</Button>
</div>
</div>
{conflicts && (
<div className="rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950 p-2 space-y-2">
<p className="text-xs font-medium text-amber-800 dark:text-amber-200">
Conflicts detected:
</p>
<ul className="text-xs text-amber-700 dark:text-amber-300 list-disc pl-4 space-y-0.5">
{conflicts.map((c, i) => (
<li key={i}>{c}</li>
))}
</ul>
<Button
variant="outline"
size="sm"
onClick={() => {
setConflicts(null);
handleRevert(true);
}}
disabled={revertMutation.isPending}
>
Force Revert
</Button>
</div>
)}
{expanded && (
<div className="pl-5 space-y-1 text-xs text-muted-foreground">
{detailQuery.isLoading && <p>Loading entries...</p>}
{entries.map((entry) => (
<div key={entry.id} className="flex items-center gap-2">
<span className="font-mono">
{entry.action === "create" ? "+" : entry.action === "delete" ? "-" : "~"}
</span>
<span>
{entry.entityType}
{entry.newState && (() => {
try {
const parsed = JSON.parse(entry.newState);
return parsed.name || parsed.title ? `: ${parsed.name || parsed.title}` : "";
} catch { return ""; }
})()}
</span>
</div>
))}
{entries.length === 0 && !detailQuery.isLoading && (
<p>No entries</p>
)}
</div>
)}
</div>
);
}