This repository has no description
1<script lang="ts">
2 import { page } from "$app/state";
3 import Check from "$icon/check";
4 import Copy from "$icon/copy";
5 import Download from "$icon/download";
6 import Checkbox from "$lib/components/ui/Checkbox.svelte";
7 import Dropdown from "$lib/components/ui/Dropdown.svelte";
8 import type { RepoInfo } from "./types";
9
10 interface Props {
11 repo: RepoInfo;
12 ref: string;
13 bobbinUrl: string;
14 }
15
16 let { repo, ref, bobbinUrl }: Props = $props();
17
18 // ssh for knot1 goes through the main domain, other knots serve their own. the
19 // knot field is usually a bare host but a scheme and port are both allowed
20 const sshHost = $derived(
21 (repo.knot === "knot1.tangled.sh" ? "tangled.org" : repo.knot)
22 .replace(/^[a-z]+:\/\//, "")
23 .split(/[:/]/)[0]
24 );
25 const origin = $derived(page.url.origin);
26
27 let permalink = $state(false);
28 let copied = $state<string | null>(null);
29
30 const urls = $derived([
31 {
32 label: "HTTPS",
33 handle: `${origin}/${repo.ownerHandle}/${repo.name}`,
34 perma: repo.repoDid ? `${origin}/${repo.repoDid}` : null
35 },
36 {
37 label: "SSH",
38 handle: `git@${sshHost}:${repo.ownerHandle}/${repo.name}`,
39 perma: repo.repoDid ? `git@${sshHost}:${repo.repoDid}` : null
40 }
41 ]);
42
43 const archiveUrl = (format: "tar.gz" | "zip") => {
44 const url = new URL("/xrpc/sh.tangled.repo.archive", `${bobbinUrl}/`);
45 url.searchParams.set("repo", repo.uri);
46 url.searchParams.set("ref", ref);
47 url.searchParams.set("format", format);
48 return url.toString();
49 };
50
51 const copy = async (label: string, value: string) => {
52 try {
53 await navigator.clipboard.writeText(value);
54 copied = label;
55 setTimeout(() => (copied = copied === label ? null : copied), 2000);
56 } catch {
57 // clipboard was denied, the text is still selectable
58 }
59 };
60</script>
61
62<Dropdown
63 variant="button"
64 buttonVariant="primary"
65 align="right"
66 label="Clone this repository"
67 menuClass="w-96 divide-y-0 p-4"
68>
69 {#snippet trigger()}
70 <Download class="size-4" aria-hidden="true" />
71 <span class="hidden md:inline">Code</span>
72 {/snippet}
73
74 <div class="flex items-center justify-between">
75 <h3 class="text-sm font-medium text-foreground-default">Clone this repository</h3>
76 {#if repo.repoDid}
77 <Checkbox bind:checked={permalink} class="typography-paragraph-small text-foreground-muted">
78 Use permalink
79 </Checkbox>
80 {/if}
81 </div>
82
83 {#each urls as entry (entry.label)}
84 {@const value = permalink && entry.perma ? entry.perma : entry.handle}
85 <div class="mt-4">
86 <span class="mb-1 block typography-paragraph-small font-medium text-foreground-muted"
87 >{entry.label}</span
88 >
89 <div
90 class="flex items-stretch divide-x divide-border-default rounded-sm border border-border-default"
91 >
92 <span
93 class="flex-1 overflow-x-auto bg-background-inset px-3 py-2 font-mono text-sm whitespace-nowrap text-foreground-default select-all"
94 >{value}</span
95 >
96 <button
97 type="button"
98 class="cursor-pointer px-3 py-2 text-foreground-subtle hover:text-foreground-default"
99 title="Copy to clipboard"
100 aria-label={`Copy ${entry.label} clone url`}
101 onclick={() => copy(entry.label, value)}
102 >
103 {#if copied === entry.label}
104 <Check class="size-4" aria-hidden="true" />
105 {:else}
106 <Copy class="size-4" aria-hidden="true" />
107 {/if}
108 </button>
109 </div>
110 </div>
111 {/each}
112
113 <p class="mt-2 typography-paragraph-small text-foreground-subtle">
114 For self-hosted knots, clone URLs may differ based on your setup.
115 </p>
116
117 <!-- archives come from bobbin, so these are external links -->
118 <div class="mt-4 flex gap-2">
119 {#each [{ format: "tar.gz" }, { format: "zip" }] as const as archive (archive.format)}
120 <a
121 href={archiveUrl(archive.format)}
122 rel="external noopener noreferrer"
123 class="flex flex-1 items-center justify-center gap-2 rounded-sm border border-border-default px-2 py-1.5 text-sm text-foreground-default no-underline hover:bg-background-subtle hover:no-underline"
124 >
125 <Download class="size-4" aria-hidden="true" />
126 {archive.format === "zip" ? ".zip" : archive.format}
127 </a>
128 {/each}
129 </div>
130</Dropdown>