change to dark mode

This commit is contained in:
2026-06-28 02:09:09 +02:00
parent 5a0a8bdecb
commit d3d52a5432
4 changed files with 596 additions and 351 deletions
+2 -1
View File
@@ -19,7 +19,8 @@
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/bt54rikhh.output)", "Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/bt54rikhh.output)",
"Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b7nyub68s.output)", "Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b7nyub68s.output)",
"Bash(npm install *)", "Bash(npm install *)",
"Bash(cargo check *)" "Bash(cargo check *)",
"Bash(npx vite *)"
] ]
} }
} }
+11 -14
View File
@@ -636,7 +636,7 @@
/> />
</aside> </aside>
<!-- Center: summary + status + commit + file history + compare --> <!-- Center: summary + status + commit + compare -->
<section class="main-panel" aria-label="Repository status"> <section class="main-panel" aria-label="Repository status">
<div class="repo-summary"> <div class="repo-summary">
<div class="repo-meta"> <div class="repo-meta">
@@ -679,18 +679,6 @@
/> />
</div> </div>
<div class="file-history-section">
<FileHistoryPanel
{fileHistory}
{selectedExplorerPath}
selectedExplorerLabel={selectedExplorerPath ? `${selectedExplorerKind === "folder" ? "Folder" : "File"} history` : "File history"}
{hasRepository}
{isBusy}
onDiff={diffSelectedFileFromCommit}
onRestore={restoreSelectedFileFromCommit}
/>
</div>
<ComparePanel <ComparePanel
{commits} {commits}
{hasRepository} {hasRepository}
@@ -707,7 +695,7 @@
/> />
</section> </section>
<!-- Right sidebar: commit history --> <!-- Right sidebar: commit graph + file history -->
<aside class="history-aside" aria-label="Commit history"> <aside class="history-aside" aria-label="Commit history">
<HistoryPanel <HistoryPanel
{commits} {commits}
@@ -722,6 +710,15 @@
expandedCommitHashes = next; expandedCommitHashes = next;
}} }}
/> />
<FileHistoryPanel
{fileHistory}
{selectedExplorerPath}
selectedExplorerLabel={selectedExplorerPath ? `${selectedExplorerKind === "folder" ? "Folder" : "File"} history` : "File history"}
{hasRepository}
{isBusy}
onDiff={diffSelectedFileFromCommit}
onRestore={restoreSelectedFileFromCommit}
/>
</aside> </aside>
</section> </section>
</div> </div>
+436 -281
View File
File diff suppressed because it is too large Load Diff
+146 -54
View File
@@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
import { ArrowRight, X } from "@lucide/svelte"; import { ArrowRight, FileCode, X } from "@lucide/svelte";
import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types"; import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";
type DiffLineKind = "meta" | "hunk" | "add" | "del" | "context"; type SplitRow =
| { type: "span"; kind: "meta" | "hunk"; text: string }
| {
type: "pair";
leftNum?: number; leftText?: string; leftKind: "del" | "context" | "empty";
rightNum?: number; rightText?: string; rightKind: "add" | "context" | "empty";
};
interface Props { interface Props {
comparison: GitCommitComparison; comparison: GitCommitComparison;
@@ -20,69 +26,115 @@
onSelectFile = () => {}, onSelectFile = () => {},
}: Props = $props(); }: Props = $props();
function statusLabel(kind: FileStatusKind): string {
return kind;
}
function displayDiffFile(file: GitDiffFile): string { function displayDiffFile(file: GitDiffFile): string {
return file.old_path ? `${file.old_path} -> ${file.path}` : file.path; return file.old_path ? `${file.old_path} ${file.path}` : file.path;
}
function diffLineKind(line: string): DiffLineKind {
if (line.startsWith("diff ") || line.startsWith("index ") || line.startsWith("--- ") ||
line.startsWith("+++ ") || line.startsWith("new file") || line.startsWith("deleted file") ||
line.startsWith("rename ") || line.startsWith("similarity ")) return "meta";
if (line.startsWith("@@")) return "hunk";
if (line.startsWith("+")) return "add";
if (line.startsWith("-")) return "del";
return "context";
}
function diffLines(patch: string): { kind: DiffLineKind; text: string }[] {
return patch.replace(/\n$/, "").split("\n").map((text) => ({ kind: diffLineKind(text), text }));
}
function stripDiffPathPrefix(value: string): string {
const trimmed = value.trim();
if (trimmed === "/dev/null") return trimmed;
if (trimmed.startsWith("a/") || trimmed.startsWith("b/")) return trimmed.slice(2);
return trimmed;
}
function patchFilePath(segment: string): string {
let plusPath = "";
let minusPath = "";
for (const line of segment.split("\n")) {
if (line.startsWith("+++ ")) plusPath = stripDiffPathPrefix(line.slice(4));
else if (line.startsWith("--- ")) minusPath = stripDiffPathPrefix(line.slice(4));
else if (line.startsWith("@@")) break;
}
return (plusPath && plusPath !== "/dev/null") ? plusPath : minusPath;
} }
function buildDiffByPath(patch: string): Map<string, string> { function buildDiffByPath(patch: string): Map<string, string> {
const map = new Map<string, string>(); const map = new Map<string, string>();
if (!patch.trim()) return map; if (!patch.trim()) return map;
const segments: string[] = [];
let current: string[] = []; let current: string[] = [];
for (const line of patch.split("\n")) { for (const line of patch.split("\n")) {
if (line.startsWith("diff --git ") && current.length > 0) { if (line.startsWith("diff --git ") && current.length > 0) {
segments.push(current.join("\n")); const seg = current.join("\n");
const p = segmentPath(seg);
if (p) map.set(p, seg);
current = []; current = [];
} }
current.push(line); current.push(line);
} }
if (current.length > 0) segments.push(current.join("\n")); if (current.length > 0) {
for (const segment of segments) { const seg = current.join("\n");
const path = patchFilePath(segment); const p = segmentPath(seg);
if (path) map.set(path, segment); if (p) map.set(p, seg);
} }
return map; return map;
} }
function segmentPath(seg: string): string {
let plus = "";
let minus = "";
for (const line of seg.split("\n")) {
if (line.startsWith("+++ ")) {
const r = line.slice(4).trim();
plus = r.startsWith("b/") ? r.slice(2) : r;
} else if (line.startsWith("--- ")) {
const r = line.slice(4).trim();
minus = r.startsWith("a/") ? r.slice(2) : r;
} else if (line.startsWith("@@")) break;
}
return (plus && plus !== "/dev/null") ? plus : minus;
}
function buildSplitRows(patch: string): SplitRow[] {
if (!patch.trim()) return [];
const lines = patch.replace(/\n$/, "").split("\n");
const rows: SplitRow[] = [];
let leftNum = 0;
let rightNum = 0;
const dels: string[] = [];
const adds: string[] = [];
function flush() {
if (dels.length === 0 && adds.length === 0) return;
const count = Math.max(dels.length, adds.length);
for (let i = 0; i < count; i++) {
const hasDel = i < dels.length;
const hasAdd = i < adds.length;
if (hasDel) leftNum++;
if (hasAdd) rightNum++;
rows.push({
type: "pair",
leftNum: hasDel ? leftNum : undefined,
leftText: hasDel ? dels[i].slice(1) : undefined,
leftKind: hasDel ? "del" : "empty",
rightNum: hasAdd ? rightNum : undefined,
rightText: hasAdd ? adds[i].slice(1) : undefined,
rightKind: hasAdd ? "add" : "empty",
});
}
dels.length = 0;
adds.length = 0;
}
for (const line of lines) {
const isMeta =
line.startsWith("diff ") || line.startsWith("index ") ||
line.startsWith("--- ") || line.startsWith("+++ ") ||
line.startsWith("new file") || line.startsWith("deleted file") ||
line.startsWith("rename ") || line.startsWith("similarity ");
if (isMeta) {
flush();
rows.push({ type: "span", kind: "meta", text: line });
} else if (line.startsWith("@@")) {
flush();
const m = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (m) { leftNum = parseInt(m[1]) - 1; rightNum = parseInt(m[2]) - 1; }
rows.push({ type: "span", kind: "hunk", text: line });
} else if (line.startsWith("-")) {
dels.push(line);
} else if (line.startsWith("+")) {
adds.push(line);
} else {
flush();
leftNum++;
rightNum++;
rows.push({
type: "pair",
leftNum, leftText: line.slice(1), leftKind: "context",
rightNum, rightText: line.slice(1), rightKind: "context",
});
}
}
flush();
return rows;
}
let diffByPath = $derived(buildDiffByPath(comparison.patch)); let diffByPath = $derived(buildDiffByPath(comparison.patch));
let selectedDiffFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null); let selectedFile = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
let selectedDiffPatch = $derived(selectedDiffFile ? (diffByPath.get(selectedDiffFile.path) ?? "") : ""); let selectedPatch = $derived(selectedFile ? (diffByPath.get(selectedFile.path) ?? "") : "");
let splitRows = $derived(buildSplitRows(selectedPatch));
</script> </script>
<div <div
@@ -91,12 +143,13 @@
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }} onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
> >
<div class="dialog" role="dialog" aria-modal="true" aria-label="Commit comparison" tabindex="-1"> <div class="dialog" role="dialog" aria-modal="true" aria-label="Commit comparison" tabindex="-1">
<header class="dialog-header"> <header class="dialog-header">
<div> <div>
<span class="eyebrow">Compare</span> <span class="eyebrow">Compare</span>
<h2 class="dialog-range"> <h2 class="dialog-range">
<span class="hash">{comparison.from_short}</span> <span class="hash">{comparison.from_short}</span>
<ArrowRight size={16} aria-hidden="true" /> <ArrowRight size={14} aria-hidden="true" />
<span class="hash">{comparison.to_short}</span> <span class="hash">{comparison.to_short}</span>
</h2> </h2>
</div> </div>
@@ -106,9 +159,11 @@
</header> </header>
{#if comparison.files.length === 0} {#if comparison.files.length === 0}
<div class="blank-state">No differences to show — these versions are identical.</div> <div class="blank-state">No differences — these versions are identical.</div>
{:else} {:else}
<div class="dialog-body"> <div class="dialog-body">
<!-- File list -->
<aside class="dialog-files" aria-label="Changed files"> <aside class="dialog-files" aria-label="Changed files">
{#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)} {#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
<button <button
@@ -118,7 +173,7 @@
onclick={() => onSelectFile(file)} onclick={() => onSelectFile(file)}
title={displayDiffFile(file)} title={displayDiffFile(file)}
> >
<span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span> <span class={`status-badge ${file.status}`}>{file.status}</span>
<strong>{displayDiffFile(file)}</strong> <strong>{displayDiffFile(file)}</strong>
<span class="diff-counts"> <span class="diff-counts">
<span class="adds">+{file.additions}</span> <span class="adds">+{file.additions}</span>
@@ -128,15 +183,52 @@
{/each} {/each}
</aside> </aside>
<div class="dialog-diff" aria-label="File diff"> <!-- Diff pane -->
{#if !selectedDiffFile} <div class="dialog-diff">
{#if !selectedFile}
<div class="blank-state">Select a file to see its changes.</div> <div class="blank-state">Select a file to see its changes.</div>
{:else if selectedDiffPatch.trim().length === 0} {:else if splitRows.length === 0}
<div class="blank-state">No textual changes for this file.</div> <div class="blank-state">No textual changes for this file.</div>
{:else} {:else}
<pre class="diff-view" aria-label="Unified diff">{#each diffLines(selectedDiffPatch) as line}<span class={`diff-line ${line.kind}`}>{line.text || " "}</span>{/each}</pre> <!-- Path bar -->
<div class="diff-header">
<FileCode size={13} aria-hidden="true" />
<span>{displayDiffFile(selectedFile)}</span>
<span class="diff-counts" style="margin-left: auto; flex-shrink: 0;">
<span class="adds">+{selectedFile.additions}</span>
<span class="dels">-{selectedFile.deletions}</span>
</span>
</div>
<!-- Column headers -->
<div class="split-col-headers">
<div class="split-col-label">
<span>Before</span>
<span class="split-col-hash">{comparison.from_short}</span>
</div>
<div class="split-col-label">
<span>After</span>
<span class="split-col-hash">{comparison.to_short}</span>
</div>
</div>
<!-- Split diff grid -->
<div class="split-diff" role="table" aria-label="Side-by-side diff">
{#each splitRows as row, i (i)}
{#if row.type === "span"}
<div class="split-span split-{row.kind}">{row.text}</div>
{:else}
<div class="split-num" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"}>{row.leftNum ?? ""}</div>
<div class="split-cell" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"}>{row.leftText ?? " "}</div>
<div class="split-divider"></div>
<div class="split-num" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"}>{row.rightNum ?? ""}</div>
<div class="split-cell" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"}>{row.rightText ?? " "}</div>
{/if}
{/each}
</div>
{/if} {/if}
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>