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="text-xs 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 text-xs font-medium text-foreground-muted">{entry.label}</span>
87 <div
88 class="flex items-stretch divide-x divide-border-default rounded border border-border-default"
89 >
90 <span
91 class="flex-1 overflow-x-auto bg-background-inset px-3 py-2 font-mono text-sm whitespace-nowrap text-foreground-default select-all"
92 >{value}</span
93 >
94 <button
95 type="button"
96 class="cursor-pointer px-3 py-2 text-foreground-subtle hover:text-foreground-default"
97 title="Copy to clipboard"
98 aria-label={`Copy ${entry.label} clone url`}
99 onclick={() => copy(entry.label, value)}
100 >
101 {#if copied === entry.label}
102 <Check class="size-4" aria-hidden="true" />
103 {:else}
104 <Copy class="size-4" aria-hidden="true" />
105 {/if}
106 </button>
107 </div>
108 </div>
109 {/each}
110
111 <p class="mt-2 text-xs text-foreground-subtle">
112 For self-hosted knots, clone URLs may differ based on your setup.
113 </p>
114
115 <!-- archives come from bobbin, so these are external links -->
116 <div class="mt-4 flex gap-2">
117 {#each [{ format: "tar.gz" }, { format: "zip" }] as const as archive (archive.format)}
118 <a
119 href={archiveUrl(archive.format)}
120 rel="external noopener noreferrer"
121 class="flex flex-1 items-center justify-center gap-2 rounded border border-border-default px-2 py-1.5 text-sm text-foreground-default no-underline hover:bg-background-subtle hover:no-underline"
122 >
123 <Download class="size-4" aria-hidden="true" />
124 {archive.format === "zip" ? ".zip" : archive.format}
125 </a>
126 {/each}
127 </div>
128</Dropdown>