${name}
${pill ? `${escapeHTML(pill)}` : ""}${escapeHTML(description)}
` : ""} ${data.notice ? `${escapeHTML(data.notice)}
` : ""} ${data.archived ? `Archived: This repository is read-only.
` : ""}(() => {
if (typeof HTMLElement === "undefined" || typeof customElements === "undefined") return;
const FORGE_DEFAULT_HOSTS = {
github: "github.com",
gitlab: "gitlab.com",
gitea: "gitea.com",
forgejo: "codeberg.org",
bitbucket: "bitbucket.org",
tangled: "tangled.org",
sourcehut: "git.sr.ht",
azure: "dev.azure.com",
generic: "",
};
const SHORTHAND_HOSTS = {
github: "github.com",
gitlab: "gitlab.com",
bitbucket: "bitbucket.org",
tangled: "tangled.org",
codeberg: "codeberg.org",
gitea: "gitea.com",
forgejo: "codeberg.org",
sourcehut: "git.sr.ht",
azure: "dev.azure.com",
};
const SHORTHAND_PATTERN = /^(github|gitlab|bitbucket|tangled|codeberg|gitea|forgejo|sourcehut|azure):(.+)$/i;
const TANGLED_URL = "https://tangled.org/";
const TANGLED_API = "https://api.tangled.org/xrpc";
const TANGLED_PUBLIC_API = "https://public.api.bsky.app/xrpc";
const CACHE_PREFIX = "repo_";
const CACHE_TTL = 15 * 60 * 1000;
const REQUEST_TIMEOUT = 10 * 1000;
const PROBE_TIMEOUT = 5 * 1000;
const GITHUB_PATH =
"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12";
const GITLAB_PATH =
"m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z";
const CODEBERG_PATH =
"M11.999.747A11.974 11.974 0 0 0 0 12.75c0 2.254.635 4.465 1.833 6.376L11.837 6.19c.072-.092.251-.092.323 0l4.178 5.402h-2.992l.065.239h3.113l.882 1.138h-3.674l.103.374h3.86l.777 1.003h-4.358l.135.483h4.593l.695.894h-5.038l.165.589h5.326l.609.785h-5.717l.182.65h6.038l.562.727h-6.397l.183.65h6.717A12.003 12.003 0 0 0 24 12.75 11.977 11.977 0 0 0 11.999.747zm3.654 19.104.182.65h5.326c.173-.204.353-.433.513-.65zm.385 1.377.18.65h3.563c.233-.198.485-.428.712-.65zm.383 1.377.182.648h1.203c.356-.204.685-.412 1.042-.648zz";
const GITEA_PATH =
"M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z";
const FORGEJO_PATH =
"M16.7773 0c1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.1175a7.0759 7.0759 0 0 1 4.148-1.4205l.1176-.001 1.3385.0002c.4973-.8827 1.4434-1.4788 2.5288-1.4788 1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.319c.8827.4973 1.4788 1.4434 1.4788 2.5287 0 1.602-1.2986 2.9005-2.9005 2.9005-1.6018 0-2.9004-1.2986-2.9004-2.9005 0-1.0853.596-2.0314 1.4788-2.5287l-.0002-9.9831c0-3.887 3.1195-7.0453 6.9915-7.108l.1176-.001h1.3385C14.7458.5962 15.692 0 16.7773 0ZM7.2227 19.9052c-.6596 0-1.1943.5347-1.1943 1.1943s.5347 1.1943 1.1943 1.1943 1.1944-.5347 1.1944-1.1943-.5348-1.1943-1.1944-1.1943Zm9.5546-10.4644c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Zm0-7.7346c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Z";
const BITBUCKET_PATH = "M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z";
const SOURCEHUT_PATH =
"M12 0C5.371 0 0 5.371 0 12s5.371 12 12 12 12-5.371 12-12S18.629 0 12 0Zm0 21.677A9.675 9.675 0 0 1 2.323 12 9.675 9.675 0 0 1 12 2.323 9.675 9.675 0 0 1 21.677 12 9.675 9.675 0 0 1 12 21.677Z";
const TANGLED_PATH =
"M15.823 23.573c-.78-.006-1.376-.233-2.022-.629-.926-.489-1.633-1.31-2.139-2.215-.803 1-1.876 1.606-3.078 1.949-.512.149-1.408.3-2.893-.24-2.141-.721-3.7-2.956-3.524-5.232-.032-.944.31-1.87.797-2.667-1.297-.698-2.355-1.87-2.76-3.308-.246-.787-.236-1.635-.145-2.444.325-1.91 1.754-3.571 3.593-4.168.734-1.678 2.336-2.93 4.15-3.184a5.5 5.5 0 0 1 3.507.699C12.845.425 15.521-.08 17.565.97c1.558.746 2.673 2.304 2.944 4.002 1.483.596 2.732 1.812 3.222 3.354.328.956.339 2.008.126 2.989-.379 1.53-1.456 2.833-2.85 3.545.003.272.896 2.236.746 3.718-.032 1.853-1.204 3.609-2.829 4.462-.948.555-2.071.544-3.101.533M11.38 18.24c1.314-.147 2.176-1.296 2.845-2.331.316-.472.558-1 .798-1.501.311.286.575.825 1.068.954.518.162 1.126.03 1.444-.442.608-1.137.308-2.51-.046-3.687-.218-.677-.504-1.371-1.048-1.85.116-.822-.369-1.655-1.058-2.085-.588.47-1.482.467-2.048-.036-1.087 1.112-2.082 1.075-3.044.194-.216-.198-.629 1.208-2.076.412-.831.698-1.476 1.37-2.047 2.339-.555 1.043-1.137 1.97-1.187 3.103-.024.662.492 1.356 1.191 1.302.699.063 1.174-.627 1.703-.914.08.925.168 1.918.48 2.82.357 1.168 1.616 1.91 2.808 1.74zm.682-3.486c-.64-.393-.332-1.246-.357-1.866.063-.747.115-1.533.45-2.214.354-.485 1.219-.298 1.257.325-.025.626-.312 1.25-.278 1.9-.074.542.054 1.151-.185 1.647-.197.274-.596.354-.887.208m-2.792-.357c-.602-.328-.41-1.153-.505-1.726.079-.663.013-1.505.567-1.97.542-.38 1.28.27 1.023.865-.273.753-.094 1.576-.088 2.339a.708.708 0 0 1-.997.492";
const GIT_PATH =
"M13.09 23.549a1.54 1.54 0 0 1-2.18 0L.451 13.089a1.54 1.54 0 0 1 0-2.179l7.191-7.19 2.733 2.733a1.85 1.85 0 0 0 .964 2.326v6.66a1.849 1.849 0 1 0 1.54 0V8.957l2.508 2.508a1.85 1.85 0 1 0 1.09-1.09l-2.634-2.634a1.85 1.85 0 0 0-2.378-2.377L8.73 2.63 10.91.451a1.54 1.54 0 0 1 2.179 0l10.459 10.46a1.54 1.54 0 0 1 0 2.179z";
const logo = (path) => ``;
const GITHUB_LOGO = logo(GITHUB_PATH);
const GITLAB_LOGO = logo(GITLAB_PATH);
const CODEBERG_LOGO = logo(CODEBERG_PATH);
const GITEA_LOGO = logo(GITEA_PATH);
const FORGEJO_LOGO = logo(FORGEJO_PATH);
const BITBUCKET_LOGO = logo(BITBUCKET_PATH);
const SOURCEHUT_LOGO = logo(SOURCEHUT_PATH);
const TANGLED_LOGO = logo(TANGLED_PATH);
const GIT_LOGO = logo(GIT_PATH);
const CARD_CSS = `
:host {
--repo-bg: #ffffff;
--repo-bg: light-dark(oklch(100% 0 0), oklch(17.1% 0 0));
--repo-border: #cecece;
--repo-border: light-dark(oklch(85.14% 0 0), oklch(26.9% 0 0));
--repo-border-width: 1px;
--repo-border-hover: #a3a3a3;
--repo-border-hover: light-dark(oklch(71.5% 0 0), oklch(37.1% 0 0));
--repo-border-strong: #a3a3a3;
--repo-border-strong: light-dark(oklch(71.5% 0 0), oklch(37.1% 0 0));
--repo-bg-muted: #f0f0f1;
--repo-bg-muted: light-dark(oklch(95.5% 0.001 286.76), oklch(23.6% 0 0));
--repo-text: #000000;
--repo-text: light-dark(oklch(4.6% 0 0), oklch(98.2% 0 0));
--repo-muted: #161616;
--repo-muted: light-dark(oklch(19.8% 0 0), oklch(84.9% 0 0));
--repo-subtle: #292929;
--repo-subtle: light-dark(oklch(28% 0 0), oklch(77.3% 0 0));
--repo-accent: #b54708;
--repo-accent: light-dark(oklch(52% 0.14 48), oklch(79% 0.12 58));
--repo-danger: #c10007;
--repo-danger: light-dark(oklch(50.5% 0.213 27.518), oklch(70.4% 0.191 22.216));
--repo-radius: 0.5rem;
--repo-radius-sm: 0.375rem;
--repo-radius-xs: 0.25rem;
--repo-font: "Geist", "IBM Plex Sans Arabic", system-ui, -apple-system, sans-serif;
--repo-font-mono: "Geist Mono", "IBM Plex Sans Arabic", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
display: block;
inline-size: 100%;
max-inline-size: 32rem;
box-sizing: border-box;
color-scheme: light dark;
color: var(--repo-text);
font-family: var(--repo-font);
font-size: 1rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
:host([data-repo-style="github"]) {
--repo-bg: light-dark(#ffffff, #0d1117);
--repo-bg-muted: light-dark(#f6f8fa, #161b22);
--repo-border: light-dark(#d0d7de, #30363d);
--repo-border-hover: light-dark(#8c959f, #8b949e);
--repo-border-strong: light-dark(#afb8c1, #6e7681);
--repo-text: light-dark(#1f2328, #e6edf3);
--repo-muted: light-dark(#656d76, #8b949e);
--repo-subtle: light-dark(#6e7781, #7d8590);
--repo-accent: light-dark(#0a852c, #0fbf3e);
--repo-danger: light-dark(#cf222e, #f85149);
}
:host([data-repo-style="gitlab"]) {
--repo-bg: light-dark(#ffffff, #18171d);
--repo-bg-muted: light-dark(#fbfafd, #28272d);
--repo-border: light-dark(#dcdcde, #3a383f);
--repo-border-hover: light-dark(#bfbfc3, #4c4b51);
--repo-border-strong: light-dark(#a4a3a8, #626168);
--repo-text: light-dark(#28272d, #ececef);
--repo-muted: light-dark(#626168, #bfbfc3);
--repo-subtle: light-dark(#737278, #a4a3a8);
--repo-accent: light-dark(#c74816, #fb8017);
--repo-danger: light-dark(#c52042, #fc7988);
}
:host([data-repo-style="codeberg"]) {
--repo-bg: light-dark(#ffffff, #171e26);
--repo-bg-muted: light-dark(#fafafa, #1d262f);
--repo-border: light-dark(#d4d4d8, #374351);
--repo-border-hover: light-dark(#a1a1aa, #5f6e80);
--repo-border-strong: light-dark(#babac1, #6d7d8f);
--repo-text: light-dark(#18181b, #d2e0f0);
--repo-muted: light-dark(#3f3f46, #c0cfe0);
--repo-subtle: light-dark(#52525b, #aebed0);
--repo-accent: light-dark(oklch(0.5534 0.1529 245.94), oklch(0.7576 0.1529 245.94));
--repo-danger: light-dark(#dc2626, #f87171);
}
:host([data-repo-style="gitea"]) {
--repo-bg: light-dark(#ffffff, #1c1f25);
--repo-bg-muted: light-dark(#f8f9fb, #151a1e);
--repo-border: light-dark(#d0d7de, #3b444a);
--repo-border-hover: light-dark(#b9c0c7, #59646c);
--repo-border-strong: light-dark(#99a0a7, #78858f);
--repo-text: light-dark(#181c21, #d1d5d8);
--repo-muted: light-dark(#40474d, #bdc3c7);
--repo-subtle: light-dark(#5b6167, #a8afb5);
--repo-accent: light-dark(#31699f, #7aa8d6);
--repo-danger: light-dark(#db2828, #e45e5e);
}
:host([data-repo-style="forgejo"]) {
--repo-bg: light-dark(#ffffff, #171e26);
--repo-bg-muted: light-dark(#fafafa, #1d262f);
--repo-border: light-dark(#d4d4d8, #374351);
--repo-border-hover: light-dark(#a1a1aa, #5f6e80);
--repo-border-strong: light-dark(#babac1, #6d7d8f);
--repo-text: light-dark(#18181b, #d2e0f0);
--repo-muted: light-dark(#3f3f46, #c0cfe0);
--repo-subtle: light-dark(#52525b, #aebed0);
--repo-accent: light-dark(#c2410c, #fb923c);
--repo-danger: light-dark(#dc2626, #f87171);
}
:host([data-repo-style="bitbucket"]) {
--repo-bg: light-dark(#ffffff, #1d2125);
--repo-bg-muted: light-dark(#f7f8f9, #22272b);
--repo-border: light-dark(#dcdfe4, #454f59);
--repo-border-hover: light-dark(#b3b9c4, #738496);
--repo-border-strong: light-dark(#8590a2, #8c9bab);
--repo-text: light-dark(#172b4d, #b6c2cf);
--repo-muted: light-dark(#44546f, #9fadbc);
--repo-subtle: light-dark(#626f86, #8c9bab);
--repo-accent: light-dark(#0c66e4, #579dff);
--repo-danger: light-dark(#ae2a19, #f87168);
}
:host([data-repo-style="tangled"]) {
--repo-bg: light-dark(#ffffff, #111827);
--repo-bg-muted: light-dark(#f9fafb, #1f2937);
--repo-border: light-dark(#e5e7eb, #374151);
--repo-border-hover: light-dark(#d1d5db, #4b5563);
--repo-border-strong: light-dark(#9ca3af, #6b7280);
--repo-text: light-dark(#111827, #f3f4f6);
--repo-muted: light-dark(#4b5563, #d1d5db);
--repo-subtle: light-dark(#6b7280, #9ca3af);
--repo-accent: light-dark(#4f46e5, #818cf8);
--repo-danger: light-dark(#dc2626, #f87171);
}
:host([data-repo-style="sourcehut"]) {
--repo-bg: light-dark(#ffffff, #212529);
--repo-bg-muted: light-dark(#f8f9fa, #131618);
--repo-border: light-dark(#dee2e6, #495057);
--repo-border-hover: light-dark(#adb5bd, #6c757d);
--repo-border-strong: light-dark(#ced4da, #adb5bd);
--repo-text: light-dark(#212529, #f8f9fa);
--repo-muted: light-dark(#6c757d, #ced4da);
--repo-subtle: light-dark(#495057, #adb5bd);
--repo-accent: light-dark(#0242f2, #3395ff);
--repo-danger: light-dark(#dc3545, #ff5c5c);
}
:host([data-repo-style="azure"]) {
--repo-bg: light-dark(#ffffff, #1f1f1f);
--repo-bg-muted: light-dark(#faf9f8, #252423);
--repo-border: light-dark(#edebe9, #3b3a39);
--repo-border-hover: light-dark(#d2d0ce, #605e5c);
--repo-border-strong: light-dark(#a19f9d, #8a8886);
--repo-text: light-dark(#201f1e, #f3f2f1);
--repo-muted: light-dark(#605e5c, #c8c6c4);
--repo-subtle: light-dark(#605e5c, #a19f9d);
--repo-accent: light-dark(#0078d4, #2899f5);
--repo-danger: light-dark(#a4262c, #f1707b);
}
:host([hidden]) { display: none; }
*, *::before, *::after { box-sizing: border-box; }
.card {
padding-block: 1rem;
padding-inline: 1.125rem;
border: var(--repo-border-width) solid var(--repo-border);
border-radius: var(--repo-radius);
background: var(--repo-bg);
}
.card:not(.skeleton):not(.error):hover,
.card:not(.skeleton):not(.error):focus-within {
border-color: var(--repo-border-hover);
}
.header {
display: flex;
align-items: center;
gap: 0.625rem;
min-inline-size: 0;
}
.title {
flex: 1 1 auto;
min-inline-size: 0;
margin: 0;
overflow: hidden;
font-family: var(--repo-font-mono);
font-size: 1.125rem;
font-weight: 500;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
a {
border-radius: var(--repo-radius-xs);
color: inherit;
text-decoration: none;
&:hover {
color: var(--repo-accent);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2rem;
}
&:focus-visible {
outline: 2px solid var(--repo-accent);
outline-offset: 3px;
}
}
}
.version {
flex: none;
padding-block: 0.1rem;
padding-inline: 0.4rem;
border-radius: var(--repo-radius-xs);
background: color-mix(in srgb, var(--repo-accent) 10%, transparent);
color: var(--repo-accent);
font-family: var(--repo-font-mono);
font-size: 0.75rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 1.4;
}
.description {
display: -webkit-box;
margin: 0;
margin-block-start: 0.4rem;
overflow: hidden;
color: var(--repo-muted);
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
}
.og-image {
display: block;
max-inline-size: 100%;
block-size: auto;
margin-block-start: 0.75rem;
border: 1px solid var(--repo-border);
border-radius: var(--repo-radius-sm);
background: var(--repo-bg-muted);
}
.warning {
margin: 0;
margin-block-start: 0.75rem;
padding-block: 0.45rem;
padding-inline: 0.6rem;
border-radius: var(--repo-radius-sm);
background: color-mix(in srgb, var(--repo-danger) 8%, var(--repo-bg));
color: var(--repo-danger);
font-size: 0.875rem;
overflow-wrap: anywhere;
}
.notice {
margin: 0;
margin-block-start: 0.75rem;
padding-block: 0.45rem;
padding-inline: 0.6rem;
border-radius: var(--repo-radius-sm);
background: var(--repo-bg-muted);
color: var(--repo-muted);
font-size: 0.875rem;
overflow-wrap: anywhere;
}
.card-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 0.75rem;
margin-block-start: 0.875rem;
padding-block-start: 0.75rem;
border-block-start: 1px solid var(--repo-border);
}
.meta {
display: flex;
flex: 1 1 14rem;
flex-wrap: wrap;
align-items: center;
gap: 0.45rem 0.8rem;
color: var(--repo-text);
font-family: var(--repo-font-mono);
font-size: 0.875rem;
font-variant-numeric: tabular-nums;
}
.metric { white-space: nowrap; }
.detail {
display: inline-flex;
align-items: baseline;
gap: 0.3rem;
min-inline-size: 0;
max-inline-size: 100%;
}
.detail-label {
flex: none;
color: var(--repo-subtle);
font-family: var(--repo-font);
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.detail-value {
min-inline-size: 0;
overflow-wrap: anywhere;
}
.detail-link {
color: var(--repo-text);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--repo-text) 30%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 0.2rem;
&:hover {
color: var(--repo-accent);
text-decoration-color: var(--repo-accent);
}
&:focus-visible {
border-radius: var(--repo-radius-xs);
outline: 2px solid var(--repo-accent);
outline-offset: 2px;
}
}
.keywords { align-items: flex-start; }
.keyword-list {
display: inline-flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.keyword {
padding-block: 0.1rem;
padding-inline: 0.4rem;
border: 1px solid var(--repo-border);
border-radius: var(--repo-radius-sm);
font-family: var(--repo-font-mono);
font-size: 0.75rem;
line-height: 1.5;
}
.view-link {
display: inline-flex;
flex: none;
align-items: center;
align-self: flex-end;
gap: 0.35rem;
margin-inline-start: auto;
color: var(--repo-text);
font-family: var(--repo-font-mono);
font-size: 0.75rem;
font-weight: 500;
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--repo-text) 30%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 0.2rem;
&:hover {
color: var(--repo-accent);
text-decoration-color: var(--repo-accent);
}
&:focus-visible {
border-radius: var(--repo-radius-xs);
outline: 2px solid var(--repo-accent);
outline-offset: 3px;
}
}
.forge-logo {
display: block;
flex: none;
block-size: 1rem;
inline-size: auto;
margin-inline-start: .5ch;
color: var(--repo-accent);
}
.badge {
display: inline-block;
padding-block: 0.1rem;
padding-inline: 0.4rem;
border: 1px solid var(--repo-border-strong);
border-radius: var(--repo-radius-sm);
background: var(--repo-bg-muted);
color: var(--repo-muted);
font-family: var(--repo-font-mono);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.5;
}
.error {
display: grid;
gap: 0.2rem;
}
.error-title {
margin: 0;
font-weight: 500;
}
.error-message {
margin: 0;
color: var(--repo-muted);
font-size: 0.875rem;
}
.skeleton {
display: grid;
gap: 0.55rem;
pointer-events: none;
}
.skeleton-head {
display: flex;
align-items: center;
gap: 0.75rem;
}
.skeleton-line {
display: block;
block-size: 0.75rem;
border-radius: var(--repo-radius-xs);
background: var(--repo-border);
background: linear-gradient(
90deg,
var(--repo-border) 25%,
color-mix(in srgb, var(--repo-border) 45%, var(--repo-bg)) 50%,
var(--repo-border) 75%
);
background-size: 200% 100%;
animation: repository-shimmer 1.2s ease-in-out infinite;
}
.skeleton-title { inline-size: 42%; block-size: 0.9rem; }
.skeleton-pill { inline-size: 3.25rem; block-size: 1.1rem; margin-inline-start: auto; }
.skeleton-desc { inline-size: 92%; }
.skeleton-desc.short { inline-size: 68%; }
.skeleton-meta {
display: flex;
gap: 0.5rem;
margin-block-start: 0.25rem;
padding-block-start: 0.75rem;
border-block-start: 1px solid var(--repo-border);
}
.skeleton-meta .skeleton-line { inline-size: 4.5rem; block-size: 0.7rem; }
.skeleton-meta .skeleton-line:last-child { inline-size: 2.5rem; margin-inline-start: auto; }
.sr-only {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@keyframes repository-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
@media (max-width: 22rem) {
.card { padding: 0.875rem; }
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.skeleton-line { animation: none; }
}
`;
let cardSheet = null;
const getCardSheet = () => {
if (!cardSheet) {
cardSheet = new CSSStyleSheet();
cardSheet.replaceSync(CARD_CSS);
}
return cardSheet;
};
const integerFormatter = new Intl.NumberFormat();
const compactFormatter = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 });
const sizeFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 });
const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" });
const LANGUAGE_NAMES = {
c: "C",
"c++": "C++",
"c#": "C#",
"f#": "F#",
"objective-c": "Objective-C",
javascript: "JavaScript",
typescript: "TypeScript",
html: "HTML",
css: "CSS",
scss: "SCSS",
sass: "Sass",
less: "Less",
shell: "Shell",
bash: "Bash",
powershell: "PowerShell",
python: "Python",
ruby: "Ruby",
rust: "Rust",
go: "Go",
java: "Java",
kotlin: "Kotlin",
swift: "Swift",
php: "PHP",
dart: "Dart",
elixir: "Elixir",
erlang: "Erlang",
haskell: "Haskell",
lua: "Lua",
r: "R",
scala: "Scala",
perl: "Perl",
clojure: "Clojure",
vue: "Vue",
svelte: "Svelte",
astro: "Astro",
dockerfile: "Dockerfile",
makefile: "Makefile",
nix: "Nix",
zig: "Zig",
julia: "Julia",
matlab: "MATLAB",
assembly: "Assembly",
cmake: "CMake",
groovy: "Groovy",
tex: "TeX",
"jupyter notebook": "Jupyter Notebook",
"vim script": "Vim Script",
"emacs lisp": "Emacs Lisp",
"visual basic .net": "Visual Basic .NET",
batchfile: "Batchfile",
hcl: "HCL",
};
const FORGE_ALIASES = {
github: "github",
gh: "github",
gitlab: "gitlab",
gl: "gitlab",
gitea: "gitea",
gogs: "gitea",
forgejo: "forgejo",
codeberg: "forgejo",
bitbucket: "bitbucket",
bb: "bitbucket",
tangled: "tangled",
sourcehut: "sourcehut",
srht: "sourcehut",
"sr.ht": "sourcehut",
azure: "azure",
"azure-devops": "azure",
azuredevops: "azure",
generic: "generic",
git: "generic",
};
const FORGE_META = {
github: { label: "GitHub", logo: GITHUB_LOGO },
gitlab: { label: "GitLab", logo: GITLAB_LOGO },
gitea: { label: "Gitea", logo: GITEA_LOGO },
forgejo: { label: "Forgejo", logo: FORGEJO_LOGO },
bitbucket: { label: "Bitbucket", logo: BITBUCKET_LOGO },
tangled: { label: "Tangled", logo: TANGLED_LOGO },
sourcehut: { label: "SourceHut", logo: SOURCEHUT_LOGO },
azure: { label: "Azure DevOps", logo: GIT_LOGO },
generic: { label: "Git", logo: GIT_LOGO },
};
const STATIC_FORGES = new Set(["sourcehut", "azure", "generic"]);
const probeCache = new Map();
const escapeHTML = (value) =>
String(value ?? "").replace(
/[&<>"']/g,
(character) =>
({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[character],
);
const sanitiser = (() => {
if (typeof Sanitizer !== "function") return null;
try {
return new Sanitizer({
allowAttributes: {
class: ["*"],
part: ["*"],
title: ["*"],
href: ["a"],
target: ["a"],
rel: ["a"],
role: ["*"],
"aria-hidden": ["*"],
focusable: ["svg"],
viewBox: ["svg"],
fill: ["path"],
d: ["path"],
transform: ["g", "path"],
src: ["img"],
alt: ["img"],
loading: ["img"],
decoding: ["img"],
},
});
} catch {
return null;
}
})();
const setContent = (element, markup) => {
if (sanitiser && typeof element.setHTML === "function") {
element.setHTML(markup, { sanitizer: sanitiser });
} else {
element.innerHTML = markup;
}
};
async function request(url, signal, timeoutMs = REQUEST_TIMEOUT) {
const timeout = AbortSignal.timeout(timeoutMs);
const response = await fetch(url, {
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
headers: { Accept: "application/json" },
});
if (!response.ok) {
const error = new Error(`Request failed with status ${response.status}`);
error.status = response.status;
error.rateLimited = response.status === 429 || (response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0");
error.retryAfter = response.headers.get("retry-after");
throw error;
}
return response;
}
const fetchJSON = async (url, signal, timeoutMs) => (await request(url, signal, timeoutMs)).json();
const fetchJSONWithResponse = async (url, signal, timeoutMs) => {
const response = await request(url, signal, timeoutMs);
return { data: await response.json(), headers: response.headers };
};
const cache = {
read(key) {
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch {
return null;
}
},
write(key, value) {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch {
// Storage can be unavailable or full.
}
},
};
const cleanText = (value) => (typeof value === "string" && value.trim() ? value.trim() : null);
const safeDecode = (value) => {
try {
return decodeURIComponent(value);
} catch {
return value;
}
};
const normaliseHost = (value) => {
let host = (value || "").trim();
if (!host) return "";
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host)) {
try {
host = new URL(host).hostname;
} catch {
return "";
}
}
return host
.replace(/^www\./i, "")
.replace(/\/.*$/, "")
.toLowerCase();
};
const normaliseForge = (value) => {
const key = (value || "").trim().toLowerCase();
return key ? FORGE_ALIASES[key] || null : null;
};
const detectForge = (host) => {
const value = host.toLowerCase().replace(/^www\./, "");
if (value === "github.com" || value.endsWith(".github.com") || value.includes("github")) return "github";
if (value === "gitlab.com" || value.includes("gitlab")) return "gitlab";
if (value === "codeberg.org" || value.includes("codeberg")) return "forgejo";
if (value === "gitea.com" || value.includes("gitea")) return "gitea";
if (value.includes("forgejo")) return "forgejo";
if (value === "bitbucket.org") return "bitbucket";
if (value === "tangled.org" || value === "tangled.sh" || value.endsWith(".tangled.org")) return "tangled";
if (value === "git.sr.ht" || value.endsWith(".sr.ht")) return "sourcehut";
if (value === "dev.azure.com" || value.endsWith(".visualstudio.com")) return "azure";
return "generic";
};
const STATIC_HOSTS = new Set(["git.kernel.org", "git.savannah.gnu.org", "git.savannah.nongnu.org", "sourceware.org", "git.zx2c4.com", "git.musl-libc.org"]);
const isStaticHost = (host) => host === "googlesource.com" || host.endsWith(".googlesource.com") || host.endsWith(".sourceware.org") || STATIC_HOSTS.has(host);
const detectForgeFromApi = (value) => {
const api = value.toLowerCase();
if (api.includes("api.github.com")) return "github";
if (api.includes("/api/v4")) return "gitlab";
if (api.includes("/api/v1")) return "gitea";
if (api.includes("/api/v3")) return "github";
if (api.includes("api.bitbucket.org")) return "bitbucket";
if (api.includes("api.tangled.org") || api.includes("/xrpc")) return "tangled";
return null;
};
const forgeMeta = (forge, host) => {
if (host && host.toLowerCase() === "codeberg.org") return { label: "Codeberg", logo: CODEBERG_LOGO };
if (forge === "generic" && host) return { label: host, logo: GIT_LOGO };
return FORGE_META[forge] || { label: host || "Git", logo: GIT_LOGO };
};
const hostStyleKey = (forge, host) => {
const detected = detectForge(host);
if (detected === "generic") return forge;
if (detected === "forgejo" && (host || "").toLowerCase() === "codeberg.org") return "codeberg";
return detected;
};
const hasHostInInput = (value) => {
const text = (value || "").trim().replace(/^git\+/, "");
if (!text) return false;
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) return true;
if (SHORTHAND_PATTERN.test(text)) return true;
if (/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.test(text)) return true;
const parts = text.replace(/^\/+/, "").replace(/\/+$/, "").split("/").filter(Boolean);
return parts.length >= 3 && (parts[0].includes(".") || parts[0].includes(":") || parts[0].toLowerCase() === "localhost");
};
const parseRepositoryInput = (input, hostOverride, defaultHost = "") => {
let value = (input || "").trim();
if (!value) return null;
value = value.replace(/^git\+/, "");
let host = "";
let path = "";
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
const shorthandMatch = !hasScheme ? value.match(SHORTHAND_PATTERN) : null;
const scpMatch = !hasScheme && !shorthandMatch ? value.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/) : null;
if (shorthandMatch) {
host = SHORTHAND_HOSTS[shorthandMatch[1].toLowerCase()];
path = shorthandMatch[2];
} else if (scpMatch) {
host = scpMatch[1];
path = scpMatch[2];
} else if (hasScheme) {
try {
const url = new URL(value);
if (!/^(https?|ssh|git):$/i.test(url.protocol)) return null;
host = url.hostname;
path = url.pathname;
} catch {
return null;
}
} else {
const trimmed = value.replace(/^\/+/, "").replace(/\/+$/, "");
const parts = trimmed.split("/").filter(Boolean);
if (!parts.length) return null;
if (parts.length >= 3 && (parts[0].includes(".") || parts[0].includes(":") || ["localhost"].includes(parts[0].toLowerCase()))) {
host = parts[0];
path = parts.slice(1).join("/");
} else {
host = normaliseHost(hostOverride) || defaultHost;
path = trimmed;
}
}
host = normaliseHost(host);
path = path.split(/[?#]/)[0].replace(/^\/+/, "").replace(/\/+$/, "");
path = path.replace(/\.git$/i, "").replace(/\/+$/, "");
const parts = path.split("/").filter(Boolean).map(safeDecode);
if (!host || !parts.length) return null;
return { host, path: parts.join("/"), parts };
};
const shapeRepository = (parsed, forge, apiAttribute, branchAttribute) => {
const parts = parsed.parts.slice();
let owner = "";
let name = "";
if (forge === "gitlab") {
const marker = parts.indexOf("-");
if (marker >= 0) parts.length = marker;
if (parts.length < 2) return null;
name = parts.pop();
owner = parts.join("/");
} else if (forge === "sourcehut") {
if (parts.length < 2) return null;
owner = parts[0];
name = parts[1];
} else if (forge === "azure") {
const marker = parts.findIndex((part) => part.toLowerCase() === "_git");
if (marker >= 2 && parts[marker + 1]) {
owner = parts.slice(0, marker).join("/");
name = parts[marker + 1];
} else {
name = parts[parts.length - 1];
owner = parts.slice(0, -1).join("/");
}
} else if (forge === "generic" && parts.length === 1) {
name = parts[0];
} else {
if (parts.length < 2) return null;
if (forge === "generic" && isStaticHost(parsed.host)) {
name = parts[parts.length - 1];
owner = parts[parts.length - 2];
} else {
owner = parts[0];
name = parts[1];
}
}
if (!name) return null;
if (!owner && !(forge === "generic" && parts.length === 1)) return null;
const api = (apiAttribute || "").trim().replace(/\/+$/, "") || null;
return {
host: parsed.host,
forge,
owner,
name,
fullName: owner ? `${owner}/${name}` : name,
path: parsed.path,
parts: parsed.parts,
api,
branch: cleanText(branchAttribute),
};
};
const probeForge = async (host, signal) => {
if (probeCache.has(host)) return probeCache.get(host);
const base = `https://${host}`;
const checks = [
["gitea", `${base}/api/v1/version`],
["gitlab", `${base}/api/v4/projects?per_page=1`],
["github", `${base}/api/v3`],
];
const results = await Promise.all(
checks.map(async ([forge, url]) => {
try {
await fetchJSON(url, signal, PROBE_TIMEOUT);
return forge;
} catch {
return null;
}
}),
);
const detected = results.find(Boolean) || "generic";
if (!signal?.aborted) probeCache.set(host, detected);
return detected;
};
const pluralise = (count, singular, plural) => (count === 1 ? singular : plural || `${singular}s`);
const formatLanguage = (value) => {
const text = cleanText(value);
if (!text) return null;
return LANGUAGE_NAMES[text.toLowerCase()] || text;
};
const normaliseLicence = (value) => {
if (typeof value === "string") return cleanText(value);
if (!value || typeof value !== "object") return null;
const name = cleanText(value.name);
if (name && name.toLowerCase() !== "other") return name;
const spdx = cleanText(value.spdx_id) || cleanText(value.spdxId);
if (spdx && spdx.toUpperCase() !== "NOASSERTION") return spdx;
return cleanText(value.key) || cleanText(value.nickname) || name;
};
const normaliseUrl = (value) => {
if (typeof value !== "string") return null;
let url = value.trim().replace(/^git\+/, "");
if (!url) return null;
const sshMatch = url.match(/^git@([^:]+):(.+)$/);
if (sshMatch) {
url = `https://${sshMatch[1]}/${sshMatch[2]}`;
} else if (/^github:/i.test(url)) {
url = `https://github.com/${url.slice(7)}`;
} else if (/^gitlab:/i.test(url)) {
url = `https://gitlab.com/${url.slice(7)}`;
} else if (/^bitbucket:/i.test(url)) {
url = `https://bitbucket.org/${url.slice(10)}`;
} else if (url.startsWith("git://")) {
url = `https://${url.slice(6)}`;
} else if (url.startsWith("ssh://git@")) {
url = `https://${url.slice(10)}`;
}
try {
const parsed = new URL(url);
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
} catch {
return null;
}
};
const resolveImageUrl = (value, base) => {
const text = cleanText(value);
if (!text) return null;
if (text.startsWith("/")) {
try {
return normaliseUrl(new URL(text, base).href);
} catch {
return null;
}
}
return normaliseUrl(text);
};
const displayUrl = (value) => {
try {
const url = new URL(value);
const path = url.pathname.replace(/\.git$/, "").replace(/\/$/, "");
return `${url.hostname.replace(/^www\./, "")}${path}`;
} catch {
return value;
}
};
const formatSize = (bytes) => {
if (!Number.isFinite(bytes) || bytes < 0) return "";
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
return `${sizeFormatter.format(value)} ${units[unit]}`;
};
const formatDate = (value) => {
const date = new Date(value);
return value && !Number.isNaN(date.getTime()) ? dateFormatter.format(date) : "";
};
const pageCount = (headers, body) => {
const link = headers.get("link");
if (link) {
const match = link.match(/[?&]page=(\d+)>;\s*rel="last"/i);
if (match) return Number(match[1]);
}
const total = headers.get("x-total");
if (total && /^\d+$/.test(total)) return Number(total);
return Array.isArray(body) ? body.length : null;
};
const renderLabelledDetail = (label, value, part, title = "") => {
if (value === null || value === undefined || value === "") return "";
return `${escapeHTML(label)}${escapeHTML(String(value))}`;
};
const renderLinkDetail = (label, href, text, part) => {
if (!href) return "";
return `${escapeHTML(label)}${escapeHTML(text)}`;
};
const renderKeywords = (keywords, label) => {
if (!Array.isArray(keywords) || !keywords.length) return "";
const visible = keywords.slice(0, 5);
const remaining = keywords.length - visible.length;
const chips = visible.map((keyword) => `${escapeHTML(keyword)}`);
if (remaining) chips.push(`+${remaining}`);
return `${escapeHTML(label)}${chips.join("")}`;
};
const renderCountMetric = (count, noun, part, description) => {
if (!Number.isFinite(count)) return "";
const label = pluralise(count, noun);
const full = `${integerFormatter.format(count)} ${label}`;
const title = description ? `${full} ยท ${description}` : full;
return `${escapeHTML(`${compactFormatter.format(count)} ${label}`)}`;
};
const renderImage = (data) => {
if (!data.imageUrl) return "";
const alt = data.imageAlt || `${data.fullName} repository preview`;
return ``;
};
const detailRenderers = {
stars: (data) => renderCountMetric(data.stars, data.starNoun || "star", "stars", ""),
forks: (data) => renderCountMetric(data.forks, "fork", "forks", ""),
watchers: (data) => renderCountMetric(data.watchers, "watcher", "watchers", ""),
issues: (data) => renderCountMetric(data.issues, "issue", "issues", data.issueDescription || ""),
pulls: (data) => renderCountMetric(data.pulls, data.pullNoun || "pull", "pulls", data.pullDescription || ""),
language: (data) => (data.language ? `${escapeHTML(data.language)}` : ""),
licence: (data) => renderLabelledDetail("Licence", data.licence, "licence"),
release: (data) => {
if (!data.release) return "";
return data.releaseUrl ? renderLinkDetail("Release", data.releaseUrl, data.release, "release") : renderLabelledDetail("Release", data.release, "release");
},
branch: (data) => renderLabelledDetail("Branch", data.branch, "branch"),
updated: (data) => renderLabelledDetail("Updated", formatDate(data.updated), "updated", data.updated || ""),
created: (data) => renderLabelledDetail("Created", formatDate(data.created), "created", data.created || ""),
pushed: (data) => renderLabelledDetail("Pushed", formatDate(data.pushed), "pushed", data.pushed || ""),
size: (data) => {
const size = formatSize(data.size);
return size ? `${escapeHTML(size)}` : "";
},
topics: (data) => renderKeywords(data.topics, "Topics"),
homepage: (data) => renderLinkDetail("Homepage", data.homepage, displayUrl(data.homepage), "homepage"),
clone: (data) => renderLinkDetail("Clone", data.cloneUrl, displayUrl(data.cloneUrl), "clone"),
owner: (data) => renderLabelledDetail("Owner", data.author || data.owner, "owner"),
forge: (data) => {
const meta = forgeMeta(data.forge, data.host);
return `${escapeHTML(meta.label)}`;
},
archived: (data) => (typeof data.archived === "boolean" ? renderLabelledDetail("Archived", data.archived ? "Yes" : "No", "archived") : ""),
mirror: (data) => (typeof data.mirror === "boolean" ? renderLabelledDetail("Mirror", data.mirror ? "Yes" : "No", "mirror") : ""),
visibility: (data) => renderLabelledDetail("Visibility", data.visibility, "visibility"),
image: () => "",
};
const DEFAULT_DETAILS = ["stars", "forks", "language"];
const ALL_DETAILS = Object.keys(detailRenderers);
const DETAIL_ALIASES = {
license: "licence",
};
const requirementsFor = (forge, details, branch) => {
const requirements = new Set();
const has = (detail) => details.includes(detail);
if (has("release")) requirements.add("release");
if (has("pulls") && ["github", "gitlab", "bitbucket", "tangled"].includes(forge)) requirements.add("pulls");
if (has("issues") && (forge === "gitlab" || forge === "tangled")) requirements.add("issues");
if (has("language") && (forge === "gitlab" || forge === "tangled")) requirements.add("language");
if ((has("stars") || has("forks") || has("watchers")) && forge === "bitbucket") requirements.add("counts");
if (has("stars") && forge === "tangled") requirements.add("stars");
if (forge === "tangled" || (branch && !STATIC_FORGES.has(forge))) requirements.add("branch");
if (has("size") && forge === "tangled") requirements.add("language");
return requirements;
};
const branchError = (branch, fullName) => {
const error = new Error(`Branch "${branch}" could not be found in "${fullName}".`);
error.code = "branch";
error.branch = branch;
return error;
};
const createStaticRepository = (target) => {
const meta = forgeMeta(target.forge, target.host);
return {
cachedAt: Date.now(),
static: true,
fetched: [],
forge: target.forge,
host: target.host,
owner: target.owner,
name: target.name,
fullName: target.fullName,
description: null,
url: `https://${target.host}/${target.parts.join("/")}`,
homepage: null,
cloneUrl: null,
stars: null,
starNoun: "star",
forks: null,
watchers: null,
issues: null,
pulls: null,
language: null,
licence: null,
release: null,
releaseUrl: null,
branch: target.branch || null,
created: null,
updated: null,
pushed: null,
size: null,
topics: [],
archived: null,
mirror: null,
visibility: null,
author: target.owner,
imageUrl: null,
imageAlt: null,
imageFallbackUrl: null,
notice: `${meta.label} does not expose repository metadata through a public cross-origin API, so only the repository link is shown.`,
};
};
const githubApiBase = (target) => target.api || (target.host === "github.com" ? "https://api.github.com" : `https://${target.host}/api/v3`);
const loadGitHub = async (target, requirements, signal) => {
const api = githubApiBase(target);
const repoPath = `${encodeURIComponent(target.owner)}/${encodeURIComponent(target.name)}`;
const repoUrl = `${api}/repos/${repoPath}`;
const raw = await fetchJSON(repoUrl, signal);
const url = normaliseUrl(raw.html_url) || `https://github.com/${target.fullName}`;
const fetched = [];
let release = null;
let releaseUrl = null;
let pulls = null;
let branch = cleanText(raw.default_branch);
let updated = cleanText(raw.updated_at);
let pushed = cleanText(raw.pushed_at);
if (requirements.has("release")) {
const latest = await fetchJSON(`${repoUrl}/releases/latest`, signal).catch(() => null);
if (latest?.tag_name) {
release = latest.tag_name;
releaseUrl = normaliseUrl(latest.html_url) || `${url}/releases/tag/${encodeURIComponent(release)}`;
} else {
const tags = await fetchJSON(`${repoUrl}/tags?per_page=1`, signal).catch(() => null);
if (Array.isArray(tags) && tags[0]?.name) {
release = tags[0].name;
releaseUrl = `${url}/releases/tag/${encodeURIComponent(release)}`;
}
}
fetched.push("release");
}
if (requirements.has("pulls")) {
const { data, headers } = await fetchJSONWithResponse(`${repoUrl}/pulls?state=open&per_page=1`, signal).catch(() => ({
data: null,
headers: new Headers(),
}));
pulls = pageCount(headers, data);
fetched.push("pulls");
}
if (target.branch) {
branch = target.branch;
const branchData = await fetchJSON(`${repoUrl}/branches/${encodeURIComponent(target.branch)}`, signal).catch((error) => {
if (error.status === 404 || error.status === 422) throw branchError(target.branch, raw.full_name || target.fullName);
return null;
});
const date = cleanText(branchData?.commit?.commit?.committer?.date) || cleanText(branchData?.commit?.commit?.author?.date);
if (date) {
updated = date;
pushed = date;
}
fetched.push("branch");
}
return {
cachedAt: Date.now(),
fetched,
forge: "github",
host: target.host,
owner: raw.owner?.login || target.owner,
name: raw.name || target.name,
fullName: raw.full_name || target.fullName,
description: cleanText(raw.description),
url,
homepage: normaliseUrl(raw.homepage),
cloneUrl: normaliseUrl(raw.clone_url),
stars: Number.isFinite(raw.stargazers_count) ? raw.stargazers_count : null,
starNoun: "star",
forks: Number.isFinite(raw.forks_count) ? raw.forks_count : null,
watchers: Number.isFinite(raw.subscribers_count) ? raw.subscribers_count : null,
issues: Number.isFinite(raw.open_issues_count) ? raw.open_issues_count : null,
issueDescription: "open issues and pull requests",
pulls,
pullNoun: "pull",
pullDescription: "open pull requests",
language: formatLanguage(raw.language),
licence: normaliseLicence(raw.license),
release,
releaseUrl,
branch,
created: cleanText(raw.created_at),
updated,
pushed,
size: Number.isFinite(raw.size) ? raw.size * 1024 : null,
topics: Array.isArray(raw.topics) ? raw.topics.filter((topic) => typeof topic === "string" && topic.trim()).map((topic) => topic.trim()) : [],
archived: Boolean(raw.archived),
mirror: Boolean(raw.mirror_url),
visibility: cleanText(raw.visibility) || (raw.private ? "private" : "public"),
author: raw.owner?.login || target.owner,
imageUrl: target.host === "github.com" ? `https://opengraph.githubassets.com/0/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.name)}` : null,
imageAlt: `${raw.full_name || target.fullName} repository preview`,
imageFallbackUrl: resolveImageUrl(raw.owner?.avatar_url, url),
};
};
const gitlabApiBase = (target) => target.api || `https://${target.host}/api/v4`;
const loadGitLab = async (target, requirements, signal) => {
const api = gitlabApiBase(target);
const projectPath = encodeURIComponent(target.fullName);
const projectUrl = `${api}/projects/${projectPath}`;
const raw = await fetchJSON(`${projectUrl}?license=true`, signal);
const url = normaliseUrl(raw.web_url) || `https://${target.host}/${target.fullName}`;
const fetched = [];
let language = null;
let issues = null;
let pulls = null;
let release = null;
let releaseUrl = null;
let branch = cleanText(raw.default_branch);
let updated = cleanText(raw.last_activity_at);
let pushed = cleanText(raw.last_activity_at);
if (requirements.has("language")) {
const languages = await fetchJSON(`${projectUrl}/languages`, signal).catch(() => null);
if (languages && typeof languages === "object") {
const top = Object.entries(languages).sort((a, b) => Number(b[1]) - Number(a[1]))[0];
language = formatLanguage(top?.[0]);
}
fetched.push("language");
}
if (requirements.has("issues")) {
const stats = await fetchJSON(`${projectUrl}/issues_statistics`, signal).catch(() => null);
issues = Number.isFinite(stats?.statistics?.counts?.opened) ? stats.statistics.counts.opened : null;
fetched.push("issues");
}
if (requirements.has("pulls")) {
const { data, headers } = await fetchJSONWithResponse(`${projectUrl}/merge_requests?state=opened&per_page=1`, signal).catch(() => ({
data: null,
headers: new Headers(),
}));
pulls = pageCount(headers, data);
fetched.push("pulls");
}
if (requirements.has("release")) {
const tags = await fetchJSON(`${projectUrl}/repository/tags?per_page=1`, signal).catch(() => null);
if (Array.isArray(tags) && tags[0]?.name) {
release = tags[0].name;
releaseUrl = `${url}/-/tags/${encodeURIComponent(release)}`;
}
fetched.push("release");
}
if (target.branch) {
branch = target.branch;
const commits = await fetchJSON(`${projectUrl}/repository/commits?ref_name=${encodeURIComponent(target.branch)}&per_page=1`, signal).catch((error) => {
if (error.status === 404) throw branchError(target.branch, raw.path_with_namespace || target.fullName);
return null;
});
if (Array.isArray(commits)) {
if (!commits.length) throw branchError(target.branch, raw.path_with_namespace || target.fullName);
const date = cleanText(commits[0].committed_date) || cleanText(commits[0].created_at);
if (date) {
updated = date;
pushed = date;
}
}
fetched.push("branch");
}
return {
cachedAt: Date.now(),
fetched,
forge: "gitlab",
host: target.host,
owner: raw.namespace?.full_path || target.owner,
name: raw.path || raw.name || target.name,
fullName: raw.path_with_namespace || target.fullName,
description: cleanText(raw.description),
url,
homepage: null,
cloneUrl: normaliseUrl(raw.http_url_to_repo),
stars: Number.isFinite(raw.star_count) ? raw.star_count : null,
starNoun: "star",
forks: Number.isFinite(raw.forks_count) ? raw.forks_count : null,
watchers: null,
issues,
issueDescription: "open issues",
pulls,
pullNoun: "merge request",
pullDescription: "open merge requests",
language,
licence: normaliseLicence(raw.license),
release,
releaseUrl,
branch,
created: cleanText(raw.created_at),
updated,
pushed,
size: null,
topics: (Array.isArray(raw.topics) && raw.topics.length ? raw.topics : Array.isArray(raw.tag_list) ? raw.tag_list : [])
.filter((topic) => typeof topic === "string" && topic.trim())
.map((topic) => topic.trim()),
archived: typeof raw.archived === "boolean" ? raw.archived : null,
mirror: typeof raw.mirror === "boolean" ? raw.mirror : null,
visibility: cleanText(raw.visibility),
author: raw.namespace?.name || raw.namespace?.full_path || target.owner,
imageUrl: resolveImageUrl(raw.avatar_url, url),
imageAlt: `${raw.path_with_namespace || target.fullName} avatar`,
imageFallbackUrl: null,
};
};
const giteaApiBase = (target) => target.api || `https://${target.host}/api/v1`;
const loadGitea = async (target, requirements, signal) => {
const api = giteaApiBase(target);
const repoUrl = `${api}/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.name)}`;
const raw = await fetchJSON(repoUrl, signal);
const url = normaliseUrl(raw.html_url) || `https://${target.host}/${target.fullName}`;
const isForgejo = target.forge !== "gitea";
const fetched = [];
let release = null;
let releaseUrl = null;
let branch = cleanText(raw.default_branch);
let updated = cleanText(raw.updated_at);
let pushed = cleanText(raw.updated_at);
if (requirements.has("release")) {
const releases = await fetchJSON(`${repoUrl}/releases?limit=1`, signal).catch(() => null);
const latest = Array.isArray(releases) ? releases[0] : null;
if (latest?.tag_name) {
release = latest.tag_name;
releaseUrl = normaliseUrl(latest.html_url) || `${url}/releases/tag/${encodeURIComponent(release)}`;
} else {
const tags = await fetchJSON(`${repoUrl}/tags?limit=1`, signal).catch(() => null);
if (Array.isArray(tags) && tags[0]?.name) {
release = tags[0].name;
releaseUrl = `${url}/releases/tag/${encodeURIComponent(release)}`;
}
}
fetched.push("release");
}
if (target.branch) {
branch = target.branch;
const branchData = await fetchJSON(`${repoUrl}/branches/${encodeURIComponent(target.branch)}`, signal).catch((error) => {
if (error.status === 404) throw branchError(target.branch, raw.full_name || target.fullName);
return null;
});
const date = cleanText(branchData?.commit?.timestamp) || cleanText(branchData?.commit?.committer?.date);
if (date) {
updated = date;
pushed = date;
}
fetched.push("branch");
}
return {
cachedAt: Date.now(),
fetched,
forge: isForgejo ? "forgejo" : "gitea",
host: target.host,
owner: raw.owner?.login || target.owner,
name: raw.name || target.name,
fullName: raw.full_name || target.fullName,
description: cleanText(raw.description),
url,
homepage: normaliseUrl(raw.website),
cloneUrl: normaliseUrl(raw.clone_url),
stars: Number.isFinite(raw.stars_count) ? raw.stars_count : null,
starNoun: "star",
forks: Number.isFinite(raw.forks_count) ? raw.forks_count : null,
watchers: Number.isFinite(raw.watchers_count) ? raw.watchers_count : null,
issues: Number.isFinite(raw.open_issues_count) ? raw.open_issues_count : null,
issueDescription: "open issues",
pulls: Number.isFinite(raw.open_pr_counter) ? raw.open_pr_counter : null,
pullNoun: "pull",
pullDescription: "open pull requests",
language: formatLanguage(raw.language),
licence: normaliseLicence(raw.license),
release,
releaseUrl,
branch,
created: cleanText(raw.created_at),
updated,
pushed,
size: Number.isFinite(raw.size) ? raw.size * 1024 : null,
topics: Array.isArray(raw.topics) ? raw.topics.filter((topic) => typeof topic === "string" && topic.trim()).map((topic) => topic.trim()) : [],
archived: Boolean(raw.archived),
mirror: Boolean(raw.mirror),
visibility: raw.private ? "private" : raw.internal ? "internal" : "public",
author: raw.owner?.login || target.owner,
imageUrl: isForgejo ? `${url}/-/summary-card` : resolveImageUrl(raw.avatar_url, url),
imageAlt: `${raw.full_name || target.fullName} ${isForgejo ? "summary card" : "avatar"}`,
imageFallbackUrl: isForgejo ? resolveImageUrl(raw.avatar_url, url) : null,
};
};
const bitbucketApiBase = (target) => target.api || "https://api.bitbucket.org/2.0";
const loadBitbucket = async (target, requirements, signal) => {
const api = bitbucketApiBase(target);
const repoUrl = `${api}/repositories/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.name)}`;
const raw = await fetchJSON(repoUrl, signal);
const url = normaliseUrl(raw.links?.html?.href) || `https://bitbucket.org/${raw.full_name || target.fullName}`;
const fetched = [];
let watchers = null;
let forks = null;
let pulls = null;
let release = null;
let releaseUrl = null;
let branch = cleanText(raw.mainbranch?.name);
let updated = cleanText(raw.updated_on);
let pushed = cleanText(raw.updated_on);
if (requirements.has("counts")) {
const [watchersData, forksData] = await Promise.all([fetchJSON(`${repoUrl}/watchers?pagelen=0`, signal).catch(() => null), fetchJSON(`${repoUrl}/forks?pagelen=0`, signal).catch(() => null)]);
watchers = Number.isFinite(watchersData?.size) ? watchersData.size : null;
forks = Number.isFinite(forksData?.size) ? forksData.size : null;
fetched.push("counts");
}
if (requirements.has("pulls")) {
const pullRequests = await fetchJSON(`${repoUrl}/pullrequests?state=OPEN&pagelen=0`, signal).catch(() => null);
pulls = Number.isFinite(pullRequests?.size) ? pullRequests.size : null;
fetched.push("pulls");
}
if (requirements.has("release")) {
const tags = await fetchJSON(`${repoUrl}/refs/tags?pagelen=1&sort=-target.date`, signal).catch(() => null);
const tag = Array.isArray(tags?.values) ? tags.values[0] : null;
if (tag?.name) {
release = tag.name;
releaseUrl = `${url}/src/${encodeURIComponent(release)}`;
}
fetched.push("release");
}
if (target.branch) {
branch = target.branch;
const commits = await fetchJSON(`${repoUrl}/commits/${encodeURIComponent(target.branch)}?pagelen=1`, signal).catch((error) => {
if (error.status === 404) throw branchError(target.branch, raw.full_name || target.fullName);
return null;
});
const values = Array.isArray(commits?.values) ? commits.values : null;
if (values) {
if (!values.length) throw branchError(target.branch, raw.full_name || target.fullName);
const date = cleanText(values[0].date);
if (date) {
updated = date;
pushed = date;
}
}
fetched.push("branch");
}
return {
cachedAt: Date.now(),
fetched,
forge: "bitbucket",
host: target.host,
owner: raw.workspace?.slug || raw.owner?.username || target.owner,
name: raw.name || raw.slug || target.name,
fullName: raw.full_name || target.fullName,
description: cleanText(raw.description),
url,
homepage: normaliseUrl(raw.website),
cloneUrl: normaliseUrl((Array.isArray(raw.links?.clone) ? raw.links.clone : []).find((entry) => entry?.name === "https")?.href),
stars: watchers,
starNoun: "watcher",
forks,
watchers,
issues: null,
pulls,
pullNoun: "pull request",
pullDescription: "open pull requests",
language: formatLanguage(raw.language),
licence: null,
release,
releaseUrl,
branch,
created: cleanText(raw.created_on),
updated,
pushed,
size: Number.isFinite(raw.size) ? raw.size : null,
topics: [],
archived: null,
mirror: null,
visibility: raw.is_private ? "private" : "public",
author: raw.owner?.display_name || raw.owner?.nickname || raw.workspace?.name || target.owner,
imageUrl: resolveImageUrl(raw.links?.avatar?.href, url),
imageAlt: `${raw.full_name || target.fullName} avatar`,
imageFallbackUrl: null,
};
};
const tangledWebUrl = (target) => `${TANGLED_URL}${target.owner}/${target.name}`;
const normaliseKnot = (value) => {
const text = cleanText(value);
return text
? text
.replace(/^https?:\/\//i, "")
.replace(/\/.*$/, "")
.toLowerCase()
: null;
};
const resolveTangledOwner = async (owner, signal) => {
if (/^did:/i.test(owner)) return owner;
const response = await fetchJSON(`${TANGLED_PUBLIC_API}/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(owner)}`, signal);
return cleanText(response?.did);
};
const loadTangled = async (target, requirements, signal) => {
const ownerDid = await resolveTangledOwner(target.owner, signal);
if (!ownerDid) {
const error = new Error(`Could not resolve the Tangled handle "${target.owner}".`);
error.status = 404;
throw error;
}
const recordUri = `at://${ownerDid}/sh.tangled.repo/${target.name}`;
const result = await fetchJSON(`${TANGLED_API}/sh.tangled.repo.getRepo?repo=${encodeURIComponent(recordUri)}`, signal);
const record = result?.value || {};
const repoDid = cleanText(record.repoDid);
const knot = normaliseKnot(record.knot);
const fetched = [];
let stars = null;
let issues = null;
let pulls = null;
let language = null;
let branch = cleanText(target.branch);
let updated = null;
let size = null;
let release = null;
let releaseUrl = null;
if (requirements.has("stars")) {
if (repoDid) {
const count = await fetchJSON(`${TANGLED_API}/sh.tangled.feed.countStars?subject=${encodeURIComponent(repoDid)}`, signal).catch(() => null);
stars = Number.isFinite(count?.count) ? count.count : null;
}
fetched.push("stars");
}
if (requirements.has("issues")) {
if (repoDid) {
const count = await fetchJSON(`${TANGLED_API}/sh.tangled.repo.countIssues?subject=${encodeURIComponent(repoDid)}`, signal).catch(() => null);
issues = Number.isFinite(count?.count) ? count.count : null;
}
fetched.push("issues");
}
if (requirements.has("pulls")) {
if (repoDid) {
const count = await fetchJSON(`${TANGLED_API}/sh.tangled.repo.countPulls?subject=${encodeURIComponent(repoDid)}`, signal).catch(() => null);
pulls = Number.isFinite(count?.count) ? count.count : null;
}
fetched.push("pulls");
}
if (knot) {
const encodedRepo = encodeURIComponent(`${ownerDid}/${target.name}`);
if (requirements.has("language")) {
const languages = await fetchJSON(`https://${knot}/xrpc/sh.tangled.repo.languages?repo=${encodedRepo}`, signal).catch(() => null);
const top = Array.isArray(languages?.languages) ? languages.languages[0] : null;
language = formatLanguage(top?.name);
size = Number.isFinite(languages?.totalSize) ? languages.totalSize : null;
fetched.push("language");
}
if (requirements.has("branch")) {
if (target.branch) {
let log = null;
try {
log = await fetchJSON(`https://${knot}/xrpc/sh.tangled.repo.log?repo=${encodedRepo}&ref=${encodeURIComponent(target.branch)}&limit=1`, signal);
} catch (error) {
if (error.status === 404) throw branchError(target.branch, target.fullName);
}
if (log) {
const commit = Array.isArray(log.commits) ? log.commits[0] : null;
if (!commit) throw branchError(target.branch, target.fullName);
updated = cleanText(commit.committer?.When) || cleanText(commit.author?.When);
}
} else {
const defaultBranch = await fetchJSON(`https://${knot}/xrpc/sh.tangled.repo.getDefaultBranch?repo=${encodedRepo}`, signal).catch(() => null);
branch = cleanText(defaultBranch?.name);
const when = cleanText(defaultBranch?.when);
updated = when && when !== "1970-01-01T00:00:00Z" ? when : null;
}
fetched.push("branch");
}
if (requirements.has("release")) {
const tags = await fetchJSON(`https://${knot}/xrpc/sh.tangled.repo.tags?repo=${encodedRepo}`, signal).catch(() => null);
const tag = Array.isArray(tags?.tags) ? tags.tags[0] : null;
if (tag?.name) {
release = tag.name;
releaseUrl = `${tangledWebUrl(target)}/tags/${encodeURIComponent(release)}`;
}
fetched.push("release");
}
} else {
if (requirements.has("language")) fetched.push("language");
if (requirements.has("branch")) fetched.push("branch");
if (requirements.has("release")) fetched.push("release");
}
return {
cachedAt: Date.now(),
fetched,
forge: "tangled",
host: target.host,
owner: target.owner,
name: target.name,
fullName: `${target.owner}/${target.name}`,
description: cleanText(record.description),
url: tangledWebUrl(target),
homepage: normaliseUrl(record.website),
cloneUrl: `https://${knot || "tangled.org"}/${target.owner}/${target.name}`,
stars,
starNoun: "star",
forks: null,
watchers: null,
issues,
issueDescription: "open issues",
pulls,
pullNoun: "pull",
pullDescription: "open pull requests",
language,
licence: null,
release,
releaseUrl,
branch,
created: cleanText(record.createdAt),
updated,
pushed: updated,
size,
topics: Array.isArray(record.topics) ? record.topics.filter((topic) => typeof topic === "string" && topic.trim()).map((topic) => topic.trim()) : [],
archived: null,
mirror: null,
visibility: "public",
author: target.owner,
imageUrl: `${tangledWebUrl(target)}/opengraph`,
imageAlt: `${target.fullName} preview`,
imageFallbackUrl: null,
};
};
const loadRepository = (target, requirements, signal) => {
switch (target.forge) {
case "github":
return loadGitHub(target, requirements, signal);
case "gitlab":
return loadGitLab(target, requirements, signal);
case "gitea":
case "forgejo":
return loadGitea(target, requirements, signal);
case "bitbucket":
return loadBitbucket(target, requirements, signal);
case "tangled":
return loadTangled(target, requirements, signal);
default:
return Promise.resolve(createStaticRepository(target));
}
};
class RepoCard extends HTMLElement {
static observedAttributes = ["repo", "host", "forge", "api", "details", "branch"];
#content;
#data = null;
#controller = null;
#requestId = 0;
#warned = new Set();
#connected = false;
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.adoptedStyleSheets = [getCardSheet()];
this.shadowRoot.innerHTML = `
${escapeHTML(description)}
` : ""} ${data.notice ? `${escapeHTML(data.notice)}
` : ""} ${data.archived ? `Archived: This repository is read-only.
` : ""}${escapeHTML(title)}