This repository has no description
1<script lang="ts">
2 import { diffFileName, type DiffFile, type DiffStat } from "$lib/api/diff";
3 import { fileStats } from "$lib/api/rawdiff";
4 import type { Snippet } from "svelte";
5 import DiffFileCard from "./DiffFileCard.svelte";
6 import DiffFileList from "./DiffFileList.svelte";
7 import DiffTopbar from "./DiffTopbar.svelte";
8 import { diffRowKey, toFileDiffMetadata } from "./fileDiff";
9 import type { DiffStyle } from "./pierre";
10 import { encodePathSegments } from "./urls";
11
12 interface Props {
13 files: DiffFile[];
14 stat?: DiffStat;
15 // side-by-side or stacked, persisted by the route in ?diff=
16 diffStyle?: DiffStyle;
17 // raw .patch/.diff targets, only present for full-hash refs
18 downloadUrls?: { patch: string; diff: string };
19 // blob route prefix for the per-file "view file" links, hidden when absent
20 blobBase?: string;
21 // server-prerendered shadow dom per row key, only for the initial ssr
22 prerendered?: Record<string, string>;
23 // round/interdiff indicator slot in the topbar (pull pages)
24 topbarCenter?: Snippet;
25 // subscription-toggle slot at the topbar's far end (pull review panel)
26 topbarActions?: Snippet;
27 // review-button slot on each file header (pull pages)
28 fileHeaderActions?: Snippet<[DiffFile]>;
29 }
30
31 let {
32 files,
33 stat,
34 diffStyle = "unified",
35 downloadUrls,
36 blobBase,
37 prerendered,
38 topbarCenter,
39 topbarActions,
40 fileHeaderActions
41 }: Props = $props();
42
43 // one structured diff per text file, binary notes stay
44 // interleaved in the original diff order
45 const rows = $derived(
46 files.map((file) => ({
47 file,
48 name: diffFileName(file),
49 key: diffRowKey(file),
50 stat: { ...fileStats(file), files_changed: 1 },
51 fileDiff: file.is_binary ? undefined : toFileDiffMetadata(file),
52 prerenderedHTML: file.is_binary ? undefined : prerendered?.[diffRowKey(file)],
53 blobUrl:
54 blobBase && file.name.new ? `${blobBase}/${encodePathSegments(file.name.new)}` : undefined
55 }))
56 );
57
58 // cards default to open
59 let openStates = $state<Record<string, boolean>>({});
60 const isOpen = (key: string) => openStates[key] ?? true;
61 const allOpen = $derived(rows.every((row) => isOpen(row.key)));
62 const setAll = (open: boolean) => {
63 openStates = Object.fromEntries(rows.map((row) => [row.key, open]));
64 };
65
66 let filesOpen = $state(true);
67</script>
68
69<div class="flex flex-col">
70 <DiffTopbar
71 {stat}
72 fileCount={files.length}
73 {diffStyle}
74 {downloadUrls}
75 {filesOpen}
76 onToggleFiles={rows.length > 0 ? () => (filesOpen = !filesOpen) : undefined}
77 {allOpen}
78 onSetAllOpen={setAll}
79 center={topbarCenter}
80 actions={topbarActions}
81 />
82
83 <div class="flex grow gap-4">
84 {#if rows.length > 0 && filesOpen}
85 <aside
86 class="sticky top-[calc(5.75rem+env(safe-area-inset-top))] hidden max-h-[calc(100vh-5.75rem-env(safe-area-inset-top))] w-fit max-w-60 shrink-0 overflow-y-auto md:block"
87 >
88 <section
89 class="mx-auto min-h-full w-full overflow-x-auto rounded border border-border-default bg-background-default px-6 py-2 typography-paragraph-regular"
90 >
91 <DiffFileList files={rows.map((row) => ({ name: row.name }))} />
92 </section>
93 </aside>
94 {/if}
95
96 <div class="flex min-w-0 flex-1 flex-col gap-4">
97 {#if rows.length === 0}
98 <div class="py-8 text-center text-foreground-muted">
99 <p>No differences found between the selected revisions.</p>
100 </div>
101 {/if}
102 {#each rows as row (row.key)}
103 <DiffFileCard
104 file={row.file}
105 name={row.name}
106 stat={row.stat}
107 fileDiff={row.fileDiff}
108 prerenderedHTML={row.prerenderedHTML}
109 {diffStyle}
110 blobUrl={row.blobUrl}
111 bind:open={() => isOpen(row.key), (open) => (openStates[row.key] = open)}
112 headerActions={fileHeaderActions}
113 />
114 {/each}
115 </div>
116 </div>
117</div>