Complete frontend design overhaul replacing achromatic shadcn/ui defaults with an indigo-branded (#6366F1), status-aware, dark-mode-enabled token system. Phase 1 — Theme Foundation: - Replace all CSS tokens in index.css with v2 light/dark mode values - Add 24 status tokens (6 statuses × 4 variants), 22 terminal tokens, 7 diff tokens, 5 shadow tokens, 9 transition/animation tokens, 10 z-index tokens, 10-step extended indigo scale - Install Geist Sans/Mono variable fonts (public/fonts/) - Extend tailwind.config.ts with all new token utilities - Add dark mode flash-prevention script in index.html - Add status-pulse and shimmer keyframe animations - Add global focus-visible styles and reduced-motion media query Phase 2 — ThemeProvider + Toggle: - ThemeProvider context with system preference listener - 3-state ThemeToggle (Sun/Monitor/Moon) - Radix tooltip primitive for tooltips - localStorage persistence with 'cw-theme' key Phase 3 — Shared Components + Token Migration: - StatusDot: mapEntityStatus() maps raw statuses to 6 semantic variants - StatusBadge: uses status token bg/fg/border classes - Badge: 6 new status variants + xs size - EmptyState, ErrorState, SaveIndicator shared patterns - CommandPalette: Cmd+K search with fuzzy matching, keyboard nav - Skeleton with shimmer animation + SkeletonCard composite layouts - KeyboardShortcutHint, NavBadge, enhanced Sonner config - Migrate ALL hardcoded Tailwind colors to token classes across AgentOutputViewer, review/*, ProgressBar, AccountCard, InitiativeHeader, DependencyIndicator, PipelineTaskCard, PreviewPanel, ChangeSetBanner, MessageCard, PhaseDetailPanel Phase 4 — App Layout Overhaul: - Single 48px row header with CW logo, nav with NavBadge counts, Cmd+K search button, ThemeToggle, HealthDot - Remove max-w-7xl from header/main; pages control own widths - ConnectionBanner for offline/reconnecting states - BrowserTitleUpdater with running/questions counts - useGlobalKeyboard (1-4 nav, Cmd+K), useConnectionStatus hooks - Per-page width wrappers (initiatives max-w-6xl, settings max-w-4xl) Phase 5 — Page-Level Token Migration: - ReviewSidebar: all hardcoded green/orange/red → status/diff tokens - CommentThread: resolved state → status-success tokens - Settings health: green → status-success-dot
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> = {
|
|
plan: "phases",
|
|
detail: "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-status-warning-border bg-status-warning-bg p-2 space-y-2">
|
|
<p className="text-xs font-medium text-status-warning-fg">
|
|
Conflicts detected:
|
|
</p>
|
|
<ul className="text-xs text-status-warning-fg 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>
|
|
);
|
|
}
|