This repository has no description
1<script lang="ts">
2 import { resolve } from "$app/paths";
3 import ChevronLeft from "$icon/chevron-left";
4 import ChevronRight from "$icon/chevron-right";
5 import Copy from "$icon/copy";
6 import CopyCheck from "$icon/copy-check";
7 import Ellipsis from "$icon/ellipsis";
8 import FolderCode from "$icon/folder-code";
9 import Avatar from "$lib/components/ui/Avatar.svelte";
10 import Button from "$lib/components/ui/Button.svelte";
11 import Tag from "$lib/components/ui/Tag.svelte";
12 import TimeAgo from "$lib/components/ui/TimeAgo.svelte";
13 import { createCopyFeedback } from "$lib/copy.svelte";
14 import type { CommitSummary } from "./types";
15
16 interface Props {
17 ownerHandle: string;
18 repoName: string;
19 ref: string;
20 commits: CommitSummary[];
21 tagsByCommit?: Record<string, string[]>;
22 page: number;
23 pageCount: number;
24 }
25
26 let { ownerHandle, repoName, ref, commits, tagsByCommit = {}, page, pageCount }: Props = $props();
27
28 const base = $derived(`/${ownerHandle}/${repoName}`);
29 const encodedRef = $derived(encodeURIComponent(ref));
30
31 const hasPrev = $derived(page > 1);
32 const hasNext = $derived(page < pageCount);
33 const pageHref = (next: number) =>
34 resolve(`${base}/commits/${encodedRef}${next > 1 ? `?page=${next}` : ""}` as "/");
35
36 let expanded = $state<Record<string, boolean>>({});
37 const copyFeedback = createCopyFeedback();
38</script>
39
40{#snippet authorCell(commit: CommitSummary)}
41 <span class="flex items-center gap-1">
42<!-- no did/handle on a commit summary, always the placeholder -->
43 <Avatar size="size-6" />
44 {#if commit.authorEmail}
45 <a href="mailto:{commit.authorEmail}" class="no-underline hover:underline">
46 {commit.authorName}
47 </a>
48 {:else}
49 <span>{commit.authorName}</span>
50 {/if}
51 </span>
52{/snippet}
53
54{#snippet shaChip(commit: CommitSummary, mobile: boolean)}
55 <a
56 href={resolve(`${base}/commit/${commit.hash}` as "/")}
57 title={commit.changeId ? `jj change id: ${commit.changeId}` : undefined}
58 class="flex items-center gap-2 rounded bg-background-inset px-2 {mobile
59 ? 'py-1'
60 : 'py-0.5'} text-foreground-muted no-underline hover:underline"
61 >
62 {commit.shortHash}
63 </a>
64{/snippet}
65
66{#snippet treeLink(commit: CommitSummary)}
67 <a
68 href={resolve(`${base}/tree/${commit.hash}` as "/")}
69 class="rounded p-1 hover:bg-background-inset"
70 title="Browse repository at this commit"
71 aria-label="Browse repository at this commit"
72 >
73 <FolderCode class="size-4" aria-hidden="true" />
74 </a>
75{/snippet}
76
77{#snippet copyButton(commit: CommitSummary)}
78 <button
79 type="button"
80 class="cursor-pointer rounded p-1 hover:bg-background-inset"
81 title="Copy SHA"
82 aria-label="Copy SHA"
83 onclick={() => void copyFeedback.copy(commit.hash)}
84 >
85 {#if copyFeedback.copied === commit.hash}
86 <CopyCheck class="size-4" aria-hidden="true" />
87 {:else}
88 <Copy class="size-4" aria-hidden="true" />
89 {/if}
90 </button>
91{/snippet}
92
93{#snippet messageCell(commit: CommitSummary)}
94 <div>
95 <a
96 href={resolve(`${base}/commit/${commit.hash}` as "/")}
97 class="text-foreground-default no-underline hover:underline"
98 >
99 {commit.subject}
100 </a>
101 {#if commit.body}
102 <button
103 type="button"
104 class="cursor-pointer rounded bg-background-inset px-1 py-0.5 hover:bg-background-muted"
105 aria-expanded={expanded[commit.hash] === true}
106 aria-label="Toggle commit body"
107 onclick={() => (expanded[commit.hash] = !expanded[commit.hash])}
108 >
109 <Ellipsis class="size-3" aria-hidden="true" />
110 </button>
111 {/if}
112 {#each tagsByCommit[commit.hash] ?? [] as name (name)}
113 <Tag color="gray" class="ml-2 font-mono">{name}</Tag>
114 {/each}
115 </div>
116 {#if commit.body && expanded[commit.hash]}
117 <p class="mt-1 typography-paragraph-regular whitespace-pre-wrap text-foreground-muted">
118 {commit.body}
119 </p>
120 {/if}
121{/snippet}
122
123<section id="commit-table" class="overflow-x-auto rounded bg-background-default px-6 py-4">
124 <h2 class="mb-4 typography-paragraph-regular font-bold">Commits</h2>
125
126 {#if commits.length === 0}
127 <p class="py-6 text-center text-foreground-subtle">No commits at {ref}.</p>
128 {:else}
129 <div class="hidden divide-y divide-border-default md:flex md:flex-col">
130 <div class="grid grid-cols-14 gap-4">
131 <div
132 class="col-span-3 py-2 text-left typography-paragraph-regular font-bold text-foreground-muted"
133 >
134 Author
135 </div>
136 <div
137 class="col-span-3 py-2 text-left typography-paragraph-regular font-bold text-foreground-muted"
138 >
139 Commit
140 </div>
141 <div
142 class="col-span-6 py-2 text-left typography-paragraph-regular font-bold text-foreground-muted"
143 >
144 Message
145 </div>
146 <div
147 class="col-span-2 justify-self-end py-2 text-left typography-paragraph-regular font-bold text-foreground-muted"
148 >
149 Date
150 </div>
151 </div>
152 {#each commits as commit (commit.hash)}
153 <div class="grid grid-cols-14 gap-4 py-3">
154 <div class="col-span-3 align-top">
155 {@render authorCell(commit)}
156 </div>
157 <div class="col-span-3 flex items-start align-top font-mono">
158 {@render shaChip(commit, false)}
159 <!-- TODO: unverified rows indent the actions by a verified-shield
160 width, but no shield renders yet. drop the ml-6 or render the
161 shield once commit verification lands -->
162 <div class="ml-6 inline-flex">
163 {@render copyButton(commit)}
164 {@render treeLink(commit)}
165 </div>
166 </div>
167 <div class="col-span-6 align-top">
168 {@render messageCell(commit)}
169 </div>
170 <div class="col-span-2 justify-self-end align-top text-foreground-muted">
171 <TimeAgo value={commit.when} />
172 </div>
173 </div>
174 {/each}
175 </div>
176
177 <div class="md:hidden">
178 {#each commits as commit, index (commit.hash)}
179 <div
180 class="relative mb-2 p-2 {index < commits.length - 1
181 ? 'border-b border-border-default'
182 : ''}"
183 >
184 <div class="flex items-center justify-between">
185 <div class="flex-1">
186 {@render messageCell(commit)}
187 </div>
188 {@render treeLink(commit)}
189 </div>
190 <div
191 class="mt-2 flex items-center gap-1 typography-paragraph-small text-foreground-muted"
192 >
193 <span class="font-mono">
194 {@render shaChip(commit, true)}
195 </span>
196 <span aria-hidden="true">·</span>
197 {@render authorCell(commit)}
198 {#if commit.when}
199 <span aria-hidden="true">·</span>
200 <TimeAgo value={commit.when} />
201 {/if}
202 </div>
203 </div>
204 {/each}
205 </div>
206 {/if}
207</section>
208
209{#if hasPrev || hasNext}
210 <div class="mt-4 flex justify-end gap-2">
211 {#if hasPrev}
212 <Button href={pageHref(page - 1)} icon={ChevronLeft} size="sm">Previous</Button>
213 {/if}
214 {#if hasNext}
215 <Button href={pageHref(page + 1)} icon={ChevronRight} iconSide="right" size="sm">Next</Button>
216 {/if}
217 </div>
218{/if}