(() => { 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 `${escapeHTML(alt)}`; }; 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 = `
`; this.#content = this.shadowRoot.querySelector("#content"); } connectedCallback() { this.#connected = true; this.#load(); } disconnectedCallback() { this.#connected = false; this.#requestId += 1; this.#controller?.abort(); this.#controller = null; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue || !this.#connected) return; if (name === "details") { if (!this.#data) { this.#load(); return; } const branch = (this.getAttribute("branch") || "").trim(); const requirements = requirementsFor(this.#data.forge, this.#details(), branch); if (this.#data.static || [...requirements].every((requirement) => Array.isArray(this.#data.fetched) && this.#data.fetched.includes(requirement))) { this.#render(this.#data); } else { this.#load(); } return; } this.#load(); } #warnUnavailable(detail, data) { const key = `${data.forge}:${data.fullName}:${detail}`; if (this.#warned.has(key)) return; this.#warned.add(key); console.warn(`[repo-card] Detail "${detail}" is unavailable for ${data.fullName}.`); } async #load() { const requestId = ++this.#requestId; const repoInput = (this.getAttribute("repo") || "").trim(); const hostOverride = (this.getAttribute("host") || "").trim(); const forgeAttribute = (this.getAttribute("forge") || "").trim(); const apiAttribute = (this.getAttribute("api") || "").trim(); const branchAttribute = (this.getAttribute("branch") || "").trim(); const details = this.#details(); this.#controller?.abort(); this.#controller = null; if (!repoInput) { this.#data = null; this.#renderError("No repository specified", "Add a repo attribute to display repository metadata."); return; } let explicitForge = null; if (forgeAttribute) { explicitForge = normaliseForge(forgeAttribute); if (!explicitForge) { this.#data = null; this.#renderError("Unknown forge", `"${forgeAttribute}" is not a supported forge.`); return; } } else if (apiAttribute) { explicitForge = detectForgeFromApi(apiAttribute); } const apiHost = (() => { if (!apiAttribute) return ""; try { return new URL(apiAttribute).hostname; } catch { return ""; } })(); const defaultHost = normaliseHost(hostOverride) || normaliseHost(apiHost) || (explicitForge ? FORGE_DEFAULT_HOSTS[explicitForge] : "") || ""; if (!defaultHost && !hasHostInInput(repoInput)) { this.#data = null; this.#renderError("No host specified", "Add a host attribute, use a forge prefix such as codeberg:owner/repo, or use a full repository URL."); return; } const parsed = parseRepositoryInput(repoInput, hostOverride, defaultHost); if (!parsed) { this.#data = null; this.#renderError("Invalid repository", "Use owner/repo with a host, a forge prefix, a repository URL, or a clone URL."); return; } this.#data = null; this.#renderLoading(); const controller = new AbortController(); this.#controller = controller; let target = null; try { let forge = explicitForge || detectForge(parsed.host); if (forge === "generic" && !explicitForge && !isStaticHost(parsed.host)) { forge = await probeForge(parsed.host, controller.signal); if (requestId !== this.#requestId) return; } target = shapeRepository(parsed, forge, apiAttribute, branchAttribute); if (!target) { this.#renderError("Invalid repository", "The repository path could not be understood."); return; } const requirements = requirementsFor(forge, details, target.branch); const cacheKey = `${CACHE_PREFIX}${forge}:${target.host.toLowerCase()}/${target.fullName}${target.branch ? `#${target.branch}` : ""}${target.api ? `@${target.api}` : ""}`; const cached = cache.read(cacheKey); const cacheAge = cached && Number.isFinite(cached.cachedAt) ? Date.now() - cached.cachedAt : Infinity; const cacheValid = cacheAge < CACHE_TTL && (cached?.static || [...requirements].every((requirement) => Array.isArray(cached?.fetched) && cached.fetched.includes(requirement))); if (cacheValid) { this.#data = cached; this.#render(cached); return; } const data = STATIC_FORGES.has(forge) ? createStaticRepository(target) : await loadRepository(target, requirements, controller.signal); if (requestId !== this.#requestId) return; cache.write(cacheKey, data); this.#data = data; this.#render(data); } catch (error) { if (error.name === "AbortError" || requestId !== this.#requestId) return; this.#data = null; const label = target ? forgeMeta(target.forge, target.host).label : explicitForge ? forgeMeta(explicitForge, parsed.host).label : parsed.host; if (error.code === "branch") { this.#renderError("Branch not found", error.message); } else if (error.status === 404) { this.#renderError("Repository not found", `"${target?.fullName || parsed.path}" could not be found on ${label}.`); } else if (error.status === 401 || error.status === 403 || error.status === 429 || error.rateLimited) { this.#renderError("Unable to access repository", `${label} rejected the request. This may be a rate limit or a private repository.`); } else { this.#renderError("Unable to load repository", `${label} could not be reached. Please try again later.`); } } finally { if (this.#controller === controller) this.#controller = null; } } #details() { const attribute = this.getAttribute("details"); if (attribute === null) return DEFAULT_DETAILS; const requested = new Set( attribute .split(/[\s,]+/) .map((detail) => detail.trim().toLowerCase()) .map((detail) => (Object.hasOwn(DETAIL_ALIASES, detail) ? DETAIL_ALIASES[detail] : detail)) .filter(Boolean), ); if (requested.has("all")) return ALL_DETAILS; return ALL_DETAILS.filter((detail) => requested.has(detail)); } #render(data) { const styleKey = hostStyleKey(data.forge, data.host); this.dataset.repoStyle = styleKey; const meta = forgeMeta(data.forge, data.host); const name = escapeHTML(data.fullName || `${data.owner}/${data.name}`); const branch = cleanText(data.branch); const branchAttribute = (this.getAttribute("branch") || "").trim(); const release = cleanText(data.release); const pill = branch || release; const pillTitle = branch ? (branchAttribute ? "Selected branch" : "Default branch") : release ? "Latest release" : ""; const description = (data.description || "").trim(); const href = escapeHTML(data.url); const details = this.#details(); const showImage = details.includes("image"); const image = showImage ? renderImage(data) : ""; const visibleDetails = details.filter((detail) => detail !== "image" && !(data.starNoun === "watcher" && detail === "watchers" && details.includes("stars"))); const metadata = visibleDetails .map((detail) => { const rendered = detailRenderers[detail]?.(data) || ""; if (!rendered && !data.static) this.#warnUnavailable(detail, data); return rendered; }) .filter(Boolean) .join(""); if (showImage && !image && !data.static) this.#warnUnavailable("image", data); setContent( this.#content, `

${name}

${pill ? `${escapeHTML(pill)}` : ""}
${image} ${description ? `

${escapeHTML(description)}

` : ""} ${data.notice ? `

${escapeHTML(data.notice)}

` : ""} ${data.archived ? `

Archived: This repository is read-only.

` : ""}
`, ); if (image && data.imageFallbackUrl) { const element = this.#content.querySelector(".og-image"); if (element) { const applyFallback = () => { if (element.src === data.imageFallbackUrl) return; element.src = data.imageFallbackUrl; element.alt = `${data.fullName} avatar`; }; element.addEventListener("error", applyFallback, { once: true }); if (element.complete && element.naturalWidth === 0) applyFallback(); } } } #renderLoading() { setContent( this.#content, `
Loading repository information...
`, ); } #renderError(title, message) { setContent( this.#content, ` `, ); } } if (!customElements.get("repo-card")) { customElements.define("repo-card", RepoCard); } })();