This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / repo / CloneDropdown.svelte
4.1 kB 130 lines
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>