web/app/%5Bowner%5D/%5Brepo%5D/diffs/%5Bnumber%5D/page.tsxblame
View source
12ffdd41"use client";
12ffdd42
12ffdd43import { useState, useEffect } from "react";
3e3af554import Link from "next/link";
12ffdd45import { useAuth } from "@/lib/auth";
12ffdd46import {
d12933e7 diffs,
12ffdd48 repos,
d12933e9 type Diff,
12ffdd410 type Comment,
12ffdd411 type Review,
12ffdd412} from "@/lib/api";
bf5fc3313import { Skeleton } from "@/app/components/skeleton";
3e3af5514
3e3af5515interface Props {
3e3af5516 params: Promise<{ owner: string; repo: string; number: string }>;
3e3af5517}
3e3af5518
3e3af5519function timeAgo(dateStr: string): string {
3e3af5520 const seconds = Math.floor(
3e3af5521 (Date.now() - new Date(dateStr).getTime()) / 1000
3e3af5522 );
3e3af5523 if (seconds < 60) return "just now";
3e3af5524 if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
3e3af5525 if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
3e3af5526 if (seconds < 2592000) return `${Math.floor(seconds / 86400)}d ago`;
3e3af5527 return new Date(dateStr).toLocaleDateString();
3e3af5528}
3e3af5529
12ffdd430interface DiffFile {
12ffdd431 path: string;
12ffdd432 hunks: DiffHunk[];
12ffdd433}
12ffdd434
12ffdd435interface DiffHunk {
12ffdd436 header: string;
12ffdd437 lines: DiffLine[];
12ffdd438}
12ffdd439
12ffdd440interface DiffLine {
12ffdd441 type: "add" | "del" | "context";
12ffdd442 content: string;
12ffdd443 oldNum: number | null;
12ffdd444 newNum: number | null;
12ffdd445}
12ffdd446
12ffdd447/** Parse unified diff hunks from a diff string (single file or full) */
12ffdd448function parseUnifiedDiff(raw: string): DiffHunk[] {
12ffdd449 const hunks: DiffHunk[] = [];
12ffdd450 let currentHunk: DiffHunk | null = null;
12ffdd451 let oldLine = 0;
12ffdd452 let newLine = 0;
12ffdd453
12ffdd454 for (const line of raw.split("\n")) {
12ffdd455 const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/);
12ffdd456 if (hunkMatch) {
12ffdd457 currentHunk = { header: line, lines: [] };
12ffdd458 hunks.push(currentHunk);
12ffdd459 oldLine = parseInt(hunkMatch[1]);
12ffdd460 newLine = parseInt(hunkMatch[2]);
12ffdd461 continue;
12ffdd462 }
12ffdd463 if (!currentHunk) continue;
12ffdd464 if (line.startsWith("+")) {
12ffdd465 currentHunk.lines.push({ type: "add", content: line.slice(1), oldNum: null, newNum: newLine++ });
12ffdd466 } else if (line.startsWith("-")) {
12ffdd467 currentHunk.lines.push({ type: "del", content: line.slice(1), oldNum: oldLine++, newNum: null });
12ffdd468 } else if (line.startsWith(" ") || line === "") {
12ffdd469 currentHunk.lines.push({ type: "context", content: line.slice(1), oldNum: oldLine++, newNum: newLine++ });
12ffdd470 }
12ffdd471 }
12ffdd472 return hunks;
12ffdd473}
12ffdd474
12ffdd475/** Parse a full multi-file git diff string */
12ffdd476function parseDiff(raw: string): DiffFile[] {
12ffdd477 const files: DiffFile[] = [];
12ffdd478 const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
12ffdd479
12ffdd480 for (const chunk of fileChunks) {
12ffdd481 const lines = chunk.split("\n");
12ffdd482 const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
12ffdd483 const path = headerMatch ? headerMatch[2] : "unknown";
12ffdd484 const hunks = parseUnifiedDiff(chunk);
12ffdd485 if (hunks.length > 0) {
12ffdd486 files.push({ path, hunks });
12ffdd487 }
12ffdd488 }
12ffdd489 return files;
12ffdd490}
12ffdd491
d12933e92export default function DiffDetailPage({ params }: Props) {
12ffdd493 const { user } = useAuth();
12ffdd494 const [owner, setOwner] = useState("");
12ffdd495 const [repo, setRepo] = useState("");
d12933e96 const [diffNumber, setDiffNumber] = useState(0);
d12933e97 const [diff, setDiff] = useState<Diff | null>(null);
12ffdd498 const [comments, setComments] = useState<Comment[]>([]);
12ffdd499 const [reviews, setReviews] = useState<Review[]>([]);
12ffdd4100 const [diffFiles, setDiffFiles] = useState<DiffFile[]>([]);
12ffdd4101 const [loading, setLoading] = useState(true);
12ffdd4102 const [activeTab, setActiveTab] = useState<"conversation" | "changes">("conversation");
3e3af55103
12ffdd4104 // Comment form
12ffdd4105 const [commentBody, setCommentBody] = useState("");
12ffdd4106 const [commenting, setCommenting] = useState(false);
12ffdd4107
12ffdd4108 // Action states
2ec6868109 const [landing, setLanding] = useState(false);
12ffdd4110 const [closing, setClosing] = useState(false);
12ffdd4111 const [actionError, setActionError] = useState("");
12ffdd4112
12ffdd4113 useEffect(() => {
12ffdd4114 params.then(({ owner, repo, number }) => {
12ffdd4115 setOwner(owner);
12ffdd4116 setRepo(repo);
d12933e117 setDiffNumber(parseInt(number));
d12933e118 document.title = `D${number} · ${repo}`;
12ffdd4119 });
12ffdd4120 }, [params]);
12ffdd4121
1da9874122 useEffect(() => {
d12933e123 if (diff) {
d12933e124 document.title = `${diff.title} D${diff.number} · ${repo}`;
1da9874125 }
d12933e126 }, [diff, owner, repo]);
1da9874127
12ffdd4128 useEffect(() => {
d12933e129 if (!owner || !diffNumber) return;
12ffdd4130 loadData();
d12933e131 }, [owner, repo, diffNumber]);
12ffdd4132
12ffdd4133 async function loadData() {
12ffdd4134 setLoading(true);
12ffdd4135 try {
d12933e136 const data = await diffs.get(owner, repo, diffNumber);
d12933e137 setDiff(data.diff);
12ffdd4138 setComments(data.comments);
12ffdd4139 setReviews(data.reviews);
12ffdd4140
12ffdd4141 // Fetch diff
d12933e142 if (data.diff.status === "open") {
12ffdd4143 try {
2ec6868144 const base = data.diff.base_commit ?? data.diff.head_commit + '^';
12ffdd4145 const diffData = await repos.diff(
12ffdd4146 owner,
12ffdd4147 repo,
2ec6868148 base,
2ec6868149 data.diff.head_commit
12ffdd4150 );
12ffdd4151 if (diffData.diffs) {
12ffdd4152 const parsed: DiffFile[] = [];
12ffdd4153 for (const d of diffData.diffs) {
12ffdd4154 if (d.is_binary) continue;
12ffdd4155 const hunks = parseUnifiedDiff(d.diff);
12ffdd4156 if (hunks.length > 0) {
12ffdd4157 parsed.push({ path: d.path, hunks });
12ffdd4158 }
12ffdd4159 }
12ffdd4160 setDiffFiles(parsed);
12ffdd4161 }
12ffdd4162 } catch {
12ffdd4163 // Diff may not be available
12ffdd4164 }
12ffdd4165 }
12ffdd4166 } catch {
d12933e167 // Diff not found
12ffdd4168 } finally {
12ffdd4169 setLoading(false);
12ffdd4170 }
12ffdd4171 }
12ffdd4172
2ec6868173 async function handleLand() {
12ffdd4174 setActionError("");
2ec6868175 setLanding(true);
12ffdd4176 try {
2ec6868177 const { diff: updated } = await diffs.land(owner, repo, diffNumber);
d12933e178 setDiff(updated);
12ffdd4179 } catch (err: unknown) {
2ec6868180 setActionError(err instanceof Error ? err.message : "Land failed");
12ffdd4181 } finally {
2ec6868182 setLanding(false);
12ffdd4183 }
12ffdd4184 }
12ffdd4185
12ffdd4186 async function handleClose() {
12ffdd4187 setActionError("");
12ffdd4188 setClosing(true);
12ffdd4189 try {
d12933e190 const { diff: updated } = await diffs.update(owner, repo, diffNumber, { status: "closed" });
d12933e191 setDiff(updated);
12ffdd4192 } catch (err: unknown) {
12ffdd4193 setActionError(err instanceof Error ? err.message : "Failed to close");
12ffdd4194 } finally {
12ffdd4195 setClosing(false);
12ffdd4196 }
12ffdd4197 }
12ffdd4198
12ffdd4199 async function handleReopen() {
12ffdd4200 setActionError("");
12ffdd4201 try {
d12933e202 const { diff: updated } = await diffs.update(owner, repo, diffNumber, { status: "open" });
d12933e203 setDiff(updated);
12ffdd4204 } catch (err: unknown) {
12ffdd4205 setActionError(err instanceof Error ? err.message : "Failed to reopen");
12ffdd4206 }
12ffdd4207 }
12ffdd4208
12ffdd4209 async function handleComment(e: React.FormEvent) {
12ffdd4210 e.preventDefault();
12ffdd4211 if (!commentBody.trim()) return;
12ffdd4212 setCommenting(true);
12ffdd4213 try {
d12933e214 const { comment } = await diffs.comment(owner, repo, diffNumber, {
12ffdd4215 body: commentBody,
12ffdd4216 });
12ffdd4217 setComments((prev) => [...prev, comment]);
12ffdd4218 setCommentBody("");
12ffdd4219 } catch {
12ffdd4220 // silently fail
12ffdd4221 } finally {
12ffdd4222 setCommenting(false);
12ffdd4223 }
12ffdd4224 }
12ffdd4225
d12933e226 if (loading || !diff) {
3e3af55227 return (
bf5fc33228 <div className="max-w-3xl mx-auto px-4 py-6">
bf5fc33229 {loading ? (
bf5fc33230 <div className="space-y-4">
bf5fc33231 <Skeleton width="200px" height="1.25rem" />
bf5fc33232 <Skeleton width="100%" height="4rem" />
bf5fc33233 <div className="flex gap-3">
bf5fc33234 <Skeleton width="80px" height="2rem" />
bf5fc33235 <Skeleton width="80px" height="2rem" />
bf5fc33236 </div>
bf5fc33237 <Skeleton width="100%" height="6rem" />
bf5fc33238 </div>
bf5fc33239 ) : (
bf5fc33240 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
d12933e241 Diff not found
bf5fc33242 </p>
bf5fc33243 )}
3e3af55244 </div>
3e3af55245 );
3e3af55246 }
3e3af55247
12ffdd4248 // Merge activity into a single timeline sorted by date
12ffdd4249 const activity = [
12ffdd4250 ...comments.map((c) => ({ ...c, kind: "comment" as const })),
12ffdd4251 ...reviews.map((r) => ({ ...r, kind: "review" as const })),
12ffdd4252 ].sort(
12ffdd4253 (a, b) =>
12ffdd4254 new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
12ffdd4255 );
3e3af55256
3e3af55257 return (
135dfe5258 <div className="max-w-3xl mx-auto px-4 py-6">
135dfe5259 <div className="text-sm mb-1">
3e3af55260 <Link
d12933e261 href={`/${owner}/${repo}/diffs`}
135dfe5262 style={{ color: "var(--text-muted)" }}
135dfe5263 className="hover:underline"
3e3af55264 >
d12933e265 Diffs
3e3af55266 </Link>
135dfe5267 </div>
cf89d3c268 <h1 className="text-lg mb-1">
d12933e269 {diff.title}
135dfe5270 <span className="font-normal" style={{ color: "var(--text-muted)" }}>
d12933e271 {" "}D{diff.number}
135dfe5272 </span>
135dfe5273 </h1>
135dfe5274 <div className="text-sm mb-6" style={{ color: "var(--text-muted)" }}>
135dfe5275 <span
135dfe5276 className="capitalize"
135dfe5277 style={{
135dfe5278 color:
d12933e279 diff.status === "open"
135dfe5280 ? "var(--status-open-text)"
2ec6868281 : diff.status === "landed"
135dfe5282 ? "var(--status-merged-text)"
135dfe5283 : "var(--status-closed-text)",
135dfe5284 }}
135dfe5285 >
d12933e286 {diff.status}
135dfe5287 </span>
135dfe5288 {" \u2014 "}
2ec6868289 {diff.author_name} submitted{" "}
2ec6868290 <code className="text-xs font-mono">{diff.head_commit?.slice(0, 12)}</code>
3e3af55291 </div>
3e3af55292
d12933e293 {diff.description && (
135dfe5294 <div
135dfe5295 className="text-sm mb-6 pb-6"
135dfe5296 style={{
135dfe5297 color: "var(--text-secondary)",
135dfe5298 borderBottom: "1px solid var(--border)",
135dfe5299 }}
135dfe5300 >
d12933e301 <p className="whitespace-pre-wrap">{diff.description}</p>
135dfe5302 </div>
135dfe5303 )}
3e3af55304
2ec6868305 {/* Land / Close actions */}
d12933e306 {user && diff.status === "open" && (
135dfe5307 <div
12ffdd4308 className="flex items-center gap-3 mb-6 pb-6"
12ffdd4309 style={{ borderBottom: "1px solid var(--border)" }}
135dfe5310 >
12ffdd4311 <button
2ec6868312 onClick={handleLand}
2ec6868313 disabled={landing}
12ffdd4314 className="text-sm px-3 py-1"
12ffdd4315 style={{
12ffdd4316 backgroundColor: "var(--status-merged-bg)",
12ffdd4317 color: "var(--status-merged-text)",
12ffdd4318 border: "none",
2ec6868319 cursor: landing ? "default" : "pointer",
2ec6868320 opacity: landing ? 0.5 : 1,
12ffdd4321 font: "inherit",
12ffdd4322 fontSize: "inherit",
12ffdd4323 }}
12ffdd4324 >
2ec6868325 {landing ? "Landing..." : "Land"}
12ffdd4326 </button>
12ffdd4327 <button
12ffdd4328 onClick={handleClose}
12ffdd4329 disabled={closing}
12ffdd4330 className="text-sm px-3 py-1"
12ffdd4331 style={{
12ffdd4332 backgroundColor: "var(--bg-inset)",
12ffdd4333 border: "1px solid var(--border)",
12ffdd4334 color: "var(--text-muted)",
12ffdd4335 cursor: closing ? "default" : "pointer",
12ffdd4336 font: "inherit",
12ffdd4337 fontSize: "inherit",
12ffdd4338 }}
12ffdd4339 >
12ffdd4340 {closing ? "Closing..." : "Close"}
12ffdd4341 </button>
12ffdd4342 {actionError && (
12ffdd4343 <span className="text-xs" style={{ color: "var(--error-text)" }}>
12ffdd4344 {actionError}
135dfe5345 </span>
135dfe5346 )}
3e3af55347 </div>
12ffdd4348 )}
3e3af55349
d12933e350 {user && diff.status === "closed" && (
135dfe5351 <div
12ffdd4352 className="mb-6 pb-6"
12ffdd4353 style={{ borderBottom: "1px solid var(--border)" }}
135dfe5354 >
12ffdd4355 <button
12ffdd4356 onClick={handleReopen}
12ffdd4357 className="text-sm px-3 py-1"
135dfe5358 style={{
12ffdd4359 backgroundColor: "var(--bg-inset)",
12ffdd4360 border: "1px solid var(--border)",
12ffdd4361 color: "var(--text-muted)",
12ffdd4362 cursor: "pointer",
12ffdd4363 font: "inherit",
12ffdd4364 fontSize: "inherit",
135dfe5365 }}
135dfe5366 >
12ffdd4367 Reopen
12ffdd4368 </button>
12ffdd4369 </div>
12ffdd4370 )}
12ffdd4371
12ffdd4372 {/* Tabs */}
12ffdd4373 <div className="flex gap-4 text-sm mb-6">
12ffdd4374 <button
12ffdd4375 onClick={() => setActiveTab("conversation")}
12ffdd4376 className="hover:underline"
12ffdd4377 style={{
12ffdd4378 color: activeTab === "conversation" ? "var(--text-primary)" : "var(--text-muted)",
12ffdd4379 fontWeight: activeTab === "conversation" ? 600 : 400,
12ffdd4380 background: "none",
12ffdd4381 border: "none",
12ffdd4382 cursor: "pointer",
12ffdd4383 font: "inherit",
12ffdd4384 fontSize: "inherit",
12ffdd4385 }}
12ffdd4386 >
12ffdd4387 Conversation
12ffdd4388 </button>
12ffdd4389 <button
12ffdd4390 onClick={() => setActiveTab("changes")}
12ffdd4391 className="hover:underline"
12ffdd4392 style={{
12ffdd4393 color: activeTab === "changes" ? "var(--text-primary)" : "var(--text-muted)",
12ffdd4394 fontWeight: activeTab === "changes" ? 600 : 400,
12ffdd4395 background: "none",
12ffdd4396 border: "none",
12ffdd4397 cursor: "pointer",
12ffdd4398 font: "inherit",
12ffdd4399 fontSize: "inherit",
12ffdd4400 }}
12ffdd4401 >
12ffdd4402 Changes{diffFiles.length > 0 && ` (${diffFiles.length})`}
12ffdd4403 </button>
12ffdd4404 </div>
12ffdd4405
12ffdd4406 {activeTab === "conversation" && (
12ffdd4407 <>
12ffdd4408 {activity.map((item) =>
12ffdd4409 item.kind === "comment" ? (
12ffdd4410 <div
12ffdd4411 key={`c-${item.id}`}
12ffdd4412 className="mb-4 pb-4"
12ffdd4413 style={{ borderBottom: "1px solid var(--divide)" }}
12ffdd4414 >
12ffdd4415 <div className="flex items-center justify-between text-xs mb-1">
12ffdd4416 <span style={{ color: "var(--text-secondary)" }}>
12ffdd4417 {item.author_name}
12ffdd4418 </span>
12ffdd4419 <span style={{ color: "var(--text-faint)" }}>
12ffdd4420 {timeAgo(item.created_at)}
12ffdd4421 </span>
12ffdd4422 </div>
12ffdd4423 {item.file_path && (
12ffdd4424 <div
12ffdd4425 className="text-xs font-mono mb-1"
12ffdd4426 style={{ color: "var(--text-muted)" }}
12ffdd4427 >
12ffdd4428 {item.file_path}
12ffdd4429 {item.line_number ? `:${item.line_number}` : ""}
12ffdd4430 </div>
12ffdd4431 )}
12ffdd4432 <p
12ffdd4433 className="text-sm whitespace-pre-wrap"
12ffdd4434 style={{ color: "var(--text-secondary)" }}
12ffdd4435 >
12ffdd4436 {item.body}
12ffdd4437 </p>
12ffdd4438 </div>
12ffdd4439 ) : (
12ffdd4440 <div
12ffdd4441 key={`r-${item.id}`}
12ffdd4442 className="mb-4 pb-4 text-sm"
12ffdd4443 style={{ borderBottom: "1px solid var(--divide)" }}
12ffdd4444 >
12ffdd4445 <span style={{ color: "var(--text-secondary)" }}>
12ffdd4446 {item.reviewer_name}
12ffdd4447 </span>{" "}
12ffdd4448 <span
12ffdd4449 style={{
12ffdd4450 color:
12ffdd4451 item.status === "approved"
12ffdd4452 ? "var(--status-open-text)"
12ffdd4453 : "var(--status-closed-text)",
12ffdd4454 }}
12ffdd4455 >
12ffdd4456 {item.status === "approved" ? "approved" : "requested changes"}
12ffdd4457 </span>
12ffdd4458 {item.body && (
12ffdd4459 <p className="mt-1" style={{ color: "var(--text-muted)" }}>
12ffdd4460 {item.body}
12ffdd4461 </p>
12ffdd4462 )}
12ffdd4463 <span className="text-xs" style={{ color: "var(--text-faint)" }}>
12ffdd4464 {" "}{timeAgo(item.created_at)}
12ffdd4465 </span>
12ffdd4466 </div>
12ffdd4467 )
12ffdd4468 )}
12ffdd4469
12ffdd4470 {activity.length === 0 && (
12ffdd4471 <p className="text-sm mb-6" style={{ color: "var(--text-faint)" }}>
12ffdd4472 No activity yet.
135dfe5473 </p>
135dfe5474 )}
135dfe5475
12ffdd4476 {/* Comment form */}
d12933e477 {user && diff.status === "open" && (
12ffdd4478 <form onSubmit={handleComment} className="mt-4">
12ffdd4479 <textarea
12ffdd4480 value={commentBody}
12ffdd4481 onChange={(e) => setCommentBody(e.target.value)}
12ffdd4482 rows={3}
12ffdd4483 placeholder="Leave a comment..."
12ffdd4484 className="w-full px-2 py-1 text-sm focus:outline-none resize-y mb-2"
12ffdd4485 style={{
12ffdd4486 backgroundColor: "var(--bg-input)",
12ffdd4487 border: "1px solid var(--border)",
12ffdd4488 color: "var(--text-primary)",
12ffdd4489 }}
12ffdd4490 />
12ffdd4491 <button
12ffdd4492 type="submit"
12ffdd4493 disabled={commenting || !commentBody.trim()}
12ffdd4494 className="text-sm px-3 py-1"
12ffdd4495 style={{
12ffdd4496 backgroundColor: "var(--accent)",
12ffdd4497 color: "var(--accent-text)",
12ffdd4498 opacity: commenting || !commentBody.trim() ? 0.5 : 1,
12ffdd4499 border: "none",
12ffdd4500 cursor: commenting ? "default" : "pointer",
12ffdd4501 font: "inherit",
12ffdd4502 fontSize: "inherit",
12ffdd4503 }}
12ffdd4504 >
12ffdd4505 {commenting ? "Commenting..." : "Comment"}
12ffdd4506 </button>
12ffdd4507 </form>
12ffdd4508 )}
12ffdd4509 </>
12ffdd4510 )}
12ffdd4511
12ffdd4512 {activeTab === "changes" && (
12ffdd4513 <div>
12ffdd4514 {diffFiles.length === 0 ? (
12ffdd4515 <p className="text-sm" style={{ color: "var(--text-faint)" }}>
12ffdd4516 No changes to display.
12ffdd4517 </p>
12ffdd4518 ) : (
12ffdd4519 diffFiles.map((file) => (
12ffdd4520 <div key={file.path} className="mb-6">
12ffdd4521 <div
12ffdd4522 className="text-sm font-mono py-2 px-3"
12ffdd4523 style={{
12ffdd4524 backgroundColor: "var(--bg-inset)",
12ffdd4525 border: "1px solid var(--border)",
12ffdd4526 borderBottom: "none",
12ffdd4527 color: "var(--text-secondary)",
12ffdd4528 }}
12ffdd4529 >
12ffdd4530 {file.path}
12ffdd4531 </div>
12ffdd4532 <div
12ffdd4533 className="overflow-x-auto"
12ffdd4534 style={{ border: "1px solid var(--border)" }}
12ffdd4535 >
12ffdd4536 <table className="w-full text-sm font-mono" style={{ borderCollapse: "collapse" }}>
12ffdd4537 <tbody>
12ffdd4538 {file.hunks.map((hunk, hi) => (
12ffdd4539 <>
12ffdd4540 <tr key={`h-${hi}`}>
12ffdd4541 <td
12ffdd4542 colSpan={3}
12ffdd4543 className="text-xs py-1 px-3"
12ffdd4544 style={{
12ffdd4545 backgroundColor: "var(--bg-inset)",
12ffdd4546 color: "var(--text-faint)",
12ffdd4547 }}
12ffdd4548 >
12ffdd4549 {hunk.header}
12ffdd4550 </td>
12ffdd4551 </tr>
12ffdd4552 {hunk.lines.map((line, li) => (
12ffdd4553 <tr
12ffdd4554 key={`${hi}-${li}`}
12ffdd4555 style={{
12ffdd4556 backgroundColor:
12ffdd4557 line.type === "add"
12ffdd4558 ? "var(--diff-add-bg, rgba(46,160,67,0.1))"
12ffdd4559 : line.type === "del"
12ffdd4560 ? "var(--diff-del-bg, rgba(248,81,73,0.1))"
12ffdd4561 : "transparent",
12ffdd4562 }}
12ffdd4563 >
12ffdd4564 <td
12ffdd4565 className="text-right select-none px-2 py-0 w-10"
12ffdd4566 style={{
12ffdd4567 color: "var(--text-faint)",
12ffdd4568 borderRight: "1px solid var(--divide)",
12ffdd4569 fontSize: "0.75rem",
12ffdd4570 }}
12ffdd4571 >
12ffdd4572 {line.oldNum ?? ""}
12ffdd4573 </td>
12ffdd4574 <td
12ffdd4575 className="text-right select-none px-2 py-0 w-10"
12ffdd4576 style={{
12ffdd4577 color: "var(--text-faint)",
12ffdd4578 borderRight: "1px solid var(--divide)",
12ffdd4579 fontSize: "0.75rem",
12ffdd4580 }}
12ffdd4581 >
12ffdd4582 {line.newNum ?? ""}
12ffdd4583 </td>
12ffdd4584 <td
12ffdd4585 className="pl-3 py-0 whitespace-pre"
12ffdd4586 style={{
12ffdd4587 color:
12ffdd4588 line.type === "add"
12ffdd4589 ? "var(--diff-add-text, #3fb950)"
12ffdd4590 : line.type === "del"
12ffdd4591 ? "var(--diff-del-text, #f85149)"
12ffdd4592 : "var(--text-secondary)",
12ffdd4593 }}
12ffdd4594 >
12ffdd4595 {line.type === "add" ? "+" : line.type === "del" ? "-" : " "}
12ffdd4596 {line.content}
12ffdd4597 </td>
12ffdd4598 </tr>
12ffdd4599 ))}
12ffdd4600 </>
12ffdd4601 ))}
12ffdd4602 </tbody>
12ffdd4603 </table>
12ffdd4604 </div>
12ffdd4605 </div>
12ffdd4606 ))
12ffdd4607 )}
12ffdd4608 </div>
135dfe5609 )}
3e3af55610 </div>
3e3af55611 );
3e3af55612}