This repository has no description
1<script lang="ts">
2 import { resolve } from "$app/paths";
3 import File from "$icon/file";
4 import FileSymlink from "$icon/file-symlink";
5 import Folder from "$icon/folder";
6 import FolderInput from "$icon/folder-input";
7 import { subjectOf } from "$lib/api/repo";
8 import TimeAgo from "$lib/components/ui/TimeAgo.svelte";
9 import type { TreeEntrySummary } from "./types";
10
11 interface Props {
12 ownerHandle: string;
13 repoName: string;
14 ref: string;
15 entries: TreeEntrySummary[];
16 path?: string;
17 withMessage?: boolean;
18 }
19
20 let { ownerHandle, repoName, ref, entries, path = "", withMessage = false }: Props = $props();
21
22 const base = $derived(`/${ownerHandle}/${repoName}`);
23 const encodedRef = $derived(encodeURIComponent(ref));
24
25 const linkFor = (entry: TreeEntrySummary) => {
26 const target = path ? `${path}/${entry.name}` : entry.name;
27 const kind = entry.kind === "directory" ? "tree" : "blob";
28 return `${base}/${kind}/${encodedRef}/${target}`;
29 };
30
31 // lucide puts fill="none" on the path itself, which beats anything inherited
32 // from the svg, so the class has to reach the path
33 const iconClassFor = (entry: TreeEntrySummary) =>
34 entry.kind === "directory" ? "size-4 shrink-0 [&_path]:fill-current" : "size-4 shrink-0";
35
36 const iconFor = (entry: TreeEntrySummary) => {
37 switch (entry.kind) {
38 case "directory":
39 return Folder;
40 case "submodule":
41 return FolderInput;
42 case "symlink":
43 return FileSymlink;
44 default:
45 return File;
46 }
47 };
48</script>
49
50<!-- the divider belongs to whatever column the tree is sat in -->
51<div class="min-w-0">
52 {#each entries as entry (entry.name)}
53 {@const Glyph = iconFor(entry)}
54 <div class={`grid items-center gap-4 py-1 ${withMessage ? "grid-cols-12" : "grid-cols-3"}`}>
55 <a
56 href={resolve(linkFor(entry) as "/")}
57 class={`flex min-w-0 items-center gap-2 text-foreground-default no-underline hover:underline ${
58 withMessage ? "col-span-8 md:col-span-4" : "col-span-2"
59 }`}
60 >
61 <span class="text-foreground-muted">
62 <Glyph class={iconClassFor(entry)} aria-hidden="true" />
63 </span>
64 <span class="truncate">{entry.name}</span>
65 </a>
66
67 {#if withMessage}
68 <div class="hidden min-w-0 md:col-span-6 md:block">
69 {#if entry.lastCommitHash && entry.lastCommitMessage}
70 <a
71 href={resolve(`${base}/commit/${entry.lastCommitHash}` as "/")}
72 class="block truncate text-foreground-subtle no-underline hover:underline"
73 >
74 {subjectOf(entry.lastCommitMessage)}
75 </a>
76 {/if}
77 </div>
78 {/if}
79
80 <div
81 class={`text-right text-sm text-foreground-subtle ${
82 withMessage ? "col-span-4 md:col-span-2" : "col-span-1"
83 }`}
84 >
85 {#if entry.lastCommitHash && entry.lastCommitWhen}
86 <a
87 href={resolve(`${base}/commit/${entry.lastCommitHash}` as "/")}
88 class="text-foreground-subtle no-underline hover:underline"
89 title={entry.lastCommitMessage}
90 >
91 <TimeAgo value={entry.lastCommitWhen} />
92 </a>
93 {/if}
94 </div>
95 </div>
96 {/each}
97</div>