Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

components.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

components.tsxBlame497 lines · 1 contributor
fc1817aClaude1import type { FC } from "hono/jsx";
06d5ffeClaude2import { html } from "hono/html";
fc1817aClaude3import type { GitCommit, GitTreeEntry, GitDiffFile } from "../git/repository";
06d5ffeClaude4import type { Repository } from "../db/schema";
fc1817aClaude5
06d5ffeClaude6export const RepoHeader: FC<{
7 owner: string;
8 repo: string;
9 starCount?: number;
10 starred?: boolean;
c81ab7aClaude11 forkCount?: number;
06d5ffeClaude12 currentUser?: string | null;
c81ab7aClaude13 forkedFrom?: string | null;
71cd5ecClaude14 archived?: boolean;
15 isTemplate?: boolean;
16}> = ({
17 owner,
18 repo,
19 starCount,
20 starred,
21 forkCount,
22 currentUser,
23 forkedFrom,
24 archived,
25 isTemplate,
26}) => (
fc1817aClaude27 <div class="repo-header">
c81ab7aClaude28 <div>
debcf27Claude29 <div class="repo-header-title">
c81ab7aClaude30 <a href={`/${owner}`} class="owner">
31 {owner}
32 </a>
33 <span class="separator">/</span>
34 <a href={`/${owner}/${repo}`} class="name">
35 {repo}
36 </a>
71cd5ecClaude37 {archived && (
38 <span
debcf27Claude39 class="repo-header-pill repo-header-pill-archived"
71cd5ecClaude40 title="Read-only: pushes and new issues/PRs disabled"
41 >
42 Archived
43 </span>
44 )}
45 {isTemplate && (
46 <span
debcf27Claude47 class="repo-header-pill repo-header-pill-template"
71cd5ecClaude48 title="This repository can be used as a template"
49 >
50 Template
51 </span>
52 )}
c81ab7aClaude53 </div>
54 {forkedFrom && (
debcf27Claude55 <div class="repo-header-fork">
c81ab7aClaude56 forked from <a href={`/${forkedFrom}`}>{forkedFrom}</a>
57 </div>
58 )}
59 </div>
06d5ffeClaude60 <div class="repo-header-actions">
c81ab7aClaude61 {currentUser && currentUser !== owner && (
45e31d0Claude62 <form method="post" action={`/${owner}/${repo}/fork`} style="display:inline">
c81ab7aClaude63 <button type="submit" class="star-btn">
64 {"\u2442"} Fork {forkCount !== undefined && forkCount > 0 ? forkCount : ""}
65 </button>
66 </form>
67 )}
06d5ffeClaude68 {starCount !== undefined && (
69 currentUser ? (
45e31d0Claude70 <form method="post" action={`/${owner}/${repo}/star`} style="display:inline">
06d5ffeClaude71 <button
72 type="submit"
73 class={`star-btn${starred ? " starred" : ""}`}
74 >
75 {starred ? "\u2605" : "\u2606"} {starCount}
76 </button>
77 </form>
78 ) : (
79 <span class="star-btn">
80 {"\u2606"} {starCount}
81 </span>
82 )
83 )}
84 </div>
fc1817aClaude85 </div>
86);
87
88export const RepoNav: FC<{
89 owner: string;
90 repo: string;
3ef4c9dClaude91 active:
92 | "code"
93 | "commits"
94 | "issues"
95 | "pulls"
96 | "releases"
eafe8c6Claude97 | "actions"
3ef4c9dClaude98 | "gates"
3cbe3d6Claude99 | "insights"
100 | "explain"
101 | "changelog"
1e162a8Claude102 | "semantic"
103 | "wiki"
0316dbbClaude104 | "projects"
105 | "settings";
fc1817aClaude106}> = ({ owner, repo, active }) => (
107 <div class="repo-nav">
06d5ffeClaude108 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
fc1817aClaude109 Code
110 </a>
79136bbClaude111 <a
112 href={`/${owner}/${repo}/issues`}
113 class={active === "issues" ? "active" : ""}
114 >
115 Issues
116 </a>
1e162a8Claude117 <a
118 href={`/${owner}/${repo}/wiki`}
119 class={active === "wiki" ? "active" : ""}
120 >
121 Wiki
122 </a>
0074234Claude123 <a
124 href={`/${owner}/${repo}/pulls`}
125 class={active === "pulls" ? "active" : ""}
126 >
127 Pull Requests
128 </a>
1e162a8Claude129 <a
130 href={`/${owner}/${repo}/projects`}
131 class={active === "projects" ? "active" : ""}
132 >
133 Projects
134 </a>
fc1817aClaude135 <a
136 href={`/${owner}/${repo}/commits`}
137 class={active === "commits" ? "active" : ""}
138 >
139 Commits
140 </a>
eafe8c6Claude141 <a
142 href={`/${owner}/${repo}/actions`}
143 class={active === "actions" ? "active" : ""}
144 >
145 Actions
146 </a>
3ef4c9dClaude147 <a
148 href={`/${owner}/${repo}/releases`}
149 class={active === "releases" ? "active" : ""}
150 >
151 Releases
152 </a>
153 <a
154 href={`/${owner}/${repo}/gates`}
155 class={active === "gates" ? "active" : ""}
156 >
157 {"\u25CF"} Gates
158 </a>
159 <a
160 href={`/${owner}/${repo}/insights`}
161 class={active === "insights" ? "active" : ""}
162 >
163 Insights
164 </a>
3cbe3d6Claude165 <a
166 href={`/${owner}/${repo}/explain`}
debcf27Claude167 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
168 style="margin-left: auto"
3cbe3d6Claude169 >
170 {"\u2728"} Explain
171 </a>
debcf27Claude172 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
3ef4c9dClaude173 {"\u2728"} Ask AI
174 </a>
14c3cc8Claude175 <a
176 href={`/${owner}/${repo}/spec`}
debcf27Claude177 class="repo-nav-ai"
14c3cc8Claude178 title="Spec to PR — paste a feature spec, AI opens a draft PR"
179 >
180 {"\u2728"} Spec
181 </a>
d8ef5efClaude182 <a
183 href={`/${owner}/${repo}/ai/tests`}
debcf27Claude184 class="repo-nav-ai"
d8ef5efClaude185 title="AI Tests \u2014 generate failing test stubs from a source file"
186 >
187 {"\u2728"} Tests
188 </a>
fc1817aClaude189 </div>
190);
191
06d5ffeClaude192export const BranchSwitcher: FC<{
193 owner: string;
194 repo: string;
195 currentRef: string;
196 branches: string[];
197 pathType: "tree" | "blob" | "commits";
198 subPath?: string;
199}> = ({ owner, repo, currentRef, branches, pathType, subPath }) => {
200 if (branches.length <= 1) {
201 return <div class="branch-selector">{currentRef}</div>;
202 }
203
204 return (
205 <div class="branch-dropdown">
206 <button class="branch-selector" type="button">
207 {currentRef} &#9662;
208 </button>
209 <div class="branch-dropdown-content">
210 {branches.map((branch) => {
211 let href: string;
212 if (pathType === "commits") {
213 href = `/${owner}/${repo}/commits/${branch}`;
214 } else if (subPath) {
215 href = `/${owner}/${repo}/${pathType}/${branch}/${subPath}`;
216 } else {
217 href = `/${owner}/${repo}/tree/${branch}`;
218 }
219 return (
220 <a
221 href={href}
222 class={branch === currentRef ? "active-branch" : ""}
223 >
224 {branch}
225 </a>
226 );
227 })}
228 </div>
229 </div>
230 );
231};
232
fc1817aClaude233export const Breadcrumb: FC<{
234 owner: string;
235 repo: string;
236 ref: string;
237 path: string;
238}> = ({ owner, repo, ref, path }) => {
239 const parts = path.split("/").filter(Boolean);
240 const crumbs: { name: string; href: string }[] = [
241 { name: repo, href: `/${owner}/${repo}/tree/${ref}` },
242 ];
243 let accumulated = "";
244 for (const part of parts) {
245 accumulated += (accumulated ? "/" : "") + part;
246 crumbs.push({
247 name: part,
248 href: `/${owner}/${repo}/tree/${ref}/${accumulated}`,
249 });
250 }
251 return (
252 <div class="breadcrumb">
253 {crumbs.map((crumb, i) => (
254 <>
255 {i > 0 && <span>/</span>}
256 {i === crumbs.length - 1 ? (
257 <strong>{crumb.name}</strong>
258 ) : (
259 <a href={crumb.href}>{crumb.name}</a>
260 )}
261 </>
262 ))}
263 </div>
264 );
265};
266
267export const FileTable: FC<{
268 entries: GitTreeEntry[];
269 owner: string;
270 repo: string;
271 ref: string;
272 path: string;
273}> = ({ entries, owner, repo, ref, path }) => (
274 <table class="file-table">
275 <tbody>
276 {entries.map((entry) => {
277 const fullPath = path ? `${path}/${entry.name}` : entry.name;
278 const href =
279 entry.type === "tree"
280 ? `/${owner}/${repo}/tree/${ref}/${fullPath}`
281 : `/${owner}/${repo}/blob/${ref}/${fullPath}`;
282 return (
283 <tr>
284 <td class="file-icon">
285 {entry.type === "tree" ? "\u{1F4C1}" : "\u{1F4C4}"}
286 </td>
287 <td class="file-name">
288 <a href={href}>{entry.name}</a>
289 </td>
290 <td style="text-align: right; color: var(--text-muted); font-size: 13px;">
291 {entry.size !== undefined ? formatSize(entry.size) : ""}
292 </td>
293 </tr>
294 );
295 })}
296 </tbody>
297 </table>
298);
299
06d5ffeClaude300export const HighlightedCode: FC<{
301 highlightedHtml: string;
302 lineCount: number;
303}> = ({ highlightedHtml, lineCount }) => {
304 const lineNums = Array.from({ length: lineCount }, (_, i) => i + 1);
305 return (
306 <div class="blob-code">
307 <table>
308 <tbody>
309 <tr>
310 <td class="line-num" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
311 <pre style="margin: 0; line-height: 1.6; font-size: 13px">
312 {lineNums.map((n) => (
313 <>
314 <span>{n}</span>
315 {"\n"}
316 </>
317 ))}
318 </pre>
319 </td>
320 <td class="line-content" style="vertical-align: top; padding-top: 0; padding-bottom: 0">
321 <pre style="margin: 0; line-height: 1.6; font-size: 13px">{html([highlightedHtml] as unknown as TemplateStringsArray)}</pre>
322 </td>
323 </tr>
324 </tbody>
325 </table>
326 </div>
327 );
328};
329
330export const PlainCode: FC<{ lines: string[] }> = ({ lines }) => (
331 <div class="blob-code">
332 <table>
333 <tbody>
334 {lines.map((line, i) => (
335 <tr>
336 <td class="line-num">{i + 1}</td>
337 <td class="line-content">{line}</td>
338 </tr>
339 ))}
340 </tbody>
341 </table>
342 </div>
343);
344
fc1817aClaude345export const CommitList: FC<{
346 commits: GitCommit[];
347 owner: string;
348 repo: string;
3951454Claude349 verifications?: Record<string, { verified: boolean; reason: string }>;
350}> = ({ commits, owner, repo, verifications }) => (
fc1817aClaude351 <div class="commit-list">
3951454Claude352 {commits.map((commit) => {
353 const v = verifications?.[commit.sha];
354 return (
355 <div class="commit-item">
356 <div>
357 <div class="commit-message">
358 <a href={`/${owner}/${repo}/commit/${commit.sha}`}>
359 {commit.message}
360 </a>
361 {v?.verified && (
362 <span
363 title="Signed with a registered key"
364 style="margin-left:8px;font-size:10px;padding:1px 6px;border-radius:3px;background:var(--green,#2ea043);color:#fff;text-transform:uppercase;letter-spacing:.4px"
365 >
366 Verified
367 </span>
368 )}
369 </div>
370 <div class="commit-meta">
371 {commit.author} committed {formatRelativeDate(commit.date)}
372 </div>
fc1817aClaude373 </div>
3951454Claude374 <a
375 href={`/${owner}/${repo}/commit/${commit.sha}`}
376 class="commit-sha"
377 >
378 {commit.sha.slice(0, 7)}
379 </a>
fc1817aClaude380 </div>
3951454Claude381 );
382 })}
fc1817aClaude383 </div>
384);
385
386export const DiffView: FC<{ raw: string; files: GitDiffFile[] }> = ({
387 raw,
388 files,
389}) => {
390 const sections = parseDiff(raw);
391
392 return (
393 <div class="diff-view">
394 <div style="margin-bottom: 16px; font-size: 14px; color: var(--text-muted);">
395 Showing{" "}
396 <strong style="color: var(--text)">{files.length}</strong> changed
397 file{files.length !== 1 ? "s" : ""} with{" "}
398 <span class="stat-add">
399 +{files.reduce((s, f) => s + f.additions, 0)}
400 </span>{" "}
401 and{" "}
402 <span class="stat-del">
403 -{files.reduce((s, f) => s + f.deletions, 0)}
404 </span>
405 </div>
406 {sections.map((section) => (
407 <div class="diff-file">
408 <div class="diff-file-header">{section.path}</div>
409 <div class="diff-content">
410 {section.lines.map((line) => {
411 let cls = "line";
412 if (line.startsWith("+")) cls += " line-add";
413 else if (line.startsWith("-")) cls += " line-del";
414 else if (line.startsWith("@@")) cls += " line-hunk";
415 return <span class={cls}>{line + "\n"}</span>;
416 })}
417 </div>
418 </div>
419 ))}
420 </div>
421 );
422};
423
06d5ffeClaude424export const RepoCard: FC<{ repo: Repository; ownerName: string }> = ({
425 repo,
426 ownerName,
427}) => (
428 <div class="card">
429 <h3>
430 <a href={`/${ownerName}/${repo.name}`}>{repo.name}</a>
431 </h3>
432 {repo.description && <p>{repo.description}</p>}
433 <div class="card-meta">
434 {repo.isPrivate && <span class="badge">Private</span>}
435 <span>{"\u2606"} {repo.starCount}</span>
436 {repo.pushedAt && (
437 <span>Updated {formatRelativeDate(repo.pushedAt.toString())}</span>
438 )}
439 </div>
440 </div>
441);
442
fc1817aClaude443function parseDiff(raw: string): Array<{ path: string; lines: string[] }> {
444 const sections: Array<{ path: string; lines: string[] }> = [];
445 const diffRegex = /^diff --git a\/(.+?) b\/.+$/;
446 let current: { path: string; lines: string[] } | null = null;
447
448 for (const line of raw.split("\n")) {
449 const match = line.match(diffRegex);
450 if (match) {
451 if (current) sections.push(current);
452 current = { path: match[1], lines: [] };
453 continue;
454 }
455 if (current && !line.startsWith("diff --git")) {
456 if (
457 line.startsWith("index ") ||
458 line.startsWith("--- ") ||
459 line.startsWith("+++ ") ||
460 line.startsWith("new file") ||
461 line.startsWith("deleted file") ||
462 line.startsWith("old mode") ||
463 line.startsWith("new mode")
464 ) {
465 continue;
466 }
467 current.lines.push(line);
468 }
469 }
470 if (current) sections.push(current);
471 return sections;
472}
473
474function formatSize(bytes: number): string {
475 if (bytes < 1024) return `${bytes} B`;
476 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
477 return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
478}
479
480function formatRelativeDate(dateStr: string): string {
481 const date = new Date(dateStr);
482 const now = new Date();
483 const diffMs = now.getTime() - date.getTime();
484 const diffMins = Math.floor(diffMs / 60000);
485 if (diffMins < 1) return "just now";
486 if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
487 const diffHours = Math.floor(diffMins / 60);
488 if (diffHours < 24)
489 return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
490 const diffDays = Math.floor(diffHours / 24);
491 if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
492 return date.toLocaleDateString("en-US", {
493 month: "short",
494 day: "numeric",
495 year: "numeric",
496 });
497}