mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Task lifecycle actions get a 25-action tip map, every dialog field and git panel control annotated; fixes two round-1 disabled-button tips that could never fire and a double data-state stamp on tab-progress.
194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { GitDiffResponse } from "@/types/git";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { cn } from "@/lib/utils";
|
|
import { FileCode, FileDiff, WrapText } from "lucide-react";
|
|
import { HelpTip } from "@/components/ui/help-tip";
|
|
|
|
interface GitDiffViewerProps {
|
|
stagedDiff: GitDiffResponse | undefined;
|
|
unstagedDiff: GitDiffResponse | undefined;
|
|
isLoadingStaged: boolean;
|
|
isLoadingUnstaged: boolean;
|
|
}
|
|
|
|
function DiffContent({
|
|
diff,
|
|
isLoading,
|
|
wrap,
|
|
}: {
|
|
diff: GitDiffResponse | undefined;
|
|
isLoading: boolean;
|
|
wrap: boolean;
|
|
}) {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="p-4 space-y-2">
|
|
{Array.from({ length: 10 }).map((_, i) => (
|
|
<Skeleton key={i} className="h-4 w-full" />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!diff || !diff.diff) {
|
|
return (
|
|
<div className="p-8 text-center text-muted-foreground">
|
|
<FileDiff className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>No changes to display</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Parse diff and colorize
|
|
const lines = diff.diff.split("\n");
|
|
|
|
return (
|
|
// overflow-x-auto is the horizontal-scroll affordance for un-wrapped long
|
|
// lines on a phone; smaller mobile font, back to the desktop size at sm+.
|
|
<ScrollArea className="h-96">
|
|
<pre
|
|
className={cn(
|
|
"p-4 font-mono text-[11px] leading-relaxed sm:text-xs",
|
|
wrap ? "whitespace-pre-wrap break-all" : "overflow-x-auto",
|
|
)}
|
|
>
|
|
{lines.map((line, i) => {
|
|
let className = "";
|
|
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
className = "bg-green-500/10 text-green-700 dark:text-green-400";
|
|
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
className = "bg-red-500/10 text-red-700 dark:text-red-400";
|
|
} else if (line.startsWith("@@")) {
|
|
className = "bg-blue-500/10 text-blue-700 dark:text-blue-400";
|
|
} else if (line.startsWith("diff") || line.startsWith("index")) {
|
|
className = "text-muted-foreground font-semibold";
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
"px-2 -mx-2",
|
|
wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre",
|
|
className,
|
|
)}
|
|
>
|
|
{line || " "}
|
|
</div>
|
|
);
|
|
})}
|
|
</pre>
|
|
</ScrollArea>
|
|
);
|
|
}
|
|
|
|
export function GitDiffViewer({
|
|
stagedDiff,
|
|
unstagedDiff,
|
|
isLoadingStaged,
|
|
isLoadingUnstaged,
|
|
}: GitDiffViewerProps) {
|
|
const stagedCount = stagedDiff?.files_changed || 0;
|
|
const unstagedCount = unstagedDiff?.files_changed || 0;
|
|
const [wrap, setWrap] = useState(false);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<HelpTip label="Green = added lines, red = removed, blue = hunk header (@@ ... @@ shows the surrounding line range).">
|
|
<CardTitle className="text-sm flex items-center gap-2 w-fit">
|
|
<FileCode className="h-4 w-4" />
|
|
Changes
|
|
</CardTitle>
|
|
</HelpTip>
|
|
<HelpTip
|
|
label={
|
|
wrap
|
|
? "Long lines wrap to fit the panel — click to scroll horizontally instead."
|
|
: "Long lines overflow with horizontal scroll — click to wrap them instead."
|
|
}
|
|
>
|
|
<Button
|
|
variant={wrap ? "secondary" : "ghost"}
|
|
size="sm"
|
|
className="h-7 px-2 text-xs"
|
|
aria-pressed={wrap}
|
|
onClick={() => setWrap((w) => !w)}
|
|
>
|
|
<WrapText className="h-3.5 w-3.5 mr-1" />
|
|
Wrap
|
|
</Button>
|
|
</HelpTip>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<Tabs defaultValue="unstaged">
|
|
<div className="px-4 border-b">
|
|
<TabsList className="h-9">
|
|
{/* HelpTip wraps an inner span, never the TabsTrigger itself —
|
|
TooltipTrigger's asChild would clobber the trigger's own
|
|
data-state and break the active-tab highlight (see
|
|
task-tabs.tsx for the fuller writeup of this bug class). */}
|
|
<TabsTrigger value="unstaged" className="text-xs gap-1">
|
|
<HelpTip label="Files changed on disk that haven't been staged for the next commit yet">
|
|
<span className="inline-flex items-center gap-1">
|
|
Working Directory
|
|
{unstagedCount > 0 && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="h-4 px-1 text-[10px]"
|
|
>
|
|
{unstagedCount}
|
|
</Badge>
|
|
)}
|
|
</span>
|
|
</HelpTip>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="staged" className="text-xs gap-1">
|
|
<HelpTip label="Files staged and ready to be included in the next commit">
|
|
<span className="inline-flex items-center gap-1">
|
|
Staged
|
|
{stagedCount > 0 && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="h-4 px-1 text-[10px]"
|
|
>
|
|
{stagedCount}
|
|
</Badge>
|
|
)}
|
|
</span>
|
|
</HelpTip>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
|
|
<TabsContent value="unstaged" className="m-0">
|
|
<DiffContent
|
|
diff={unstagedDiff}
|
|
isLoading={isLoadingUnstaged}
|
|
wrap={wrap}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="staged" className="m-0">
|
|
<DiffContent
|
|
diff={stagedDiff}
|
|
isLoading={isLoadingStaged}
|
|
wrap={wrap}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|