Files
Codewalkers/apps/web/src/components/ChangeSetBanner.tsx
Lukas May 04c212da92 feat: Implement v2 design system with indigo brand, dark mode, and status tokens
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
2026-03-03 11:43:09 +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> = {
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>
);
}