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>
140 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|