Add GitHub Releases Tab

Add Releases tab after Code tab on GitHub repos

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Add GitHub Releases Tab
// @namespace    https://github.com/nvbangg/nvbangg-projects
// @version      1.7
// @description  Add Releases tab after Code tab on GitHub repos
// @author       nvbangg (https://github.com/nvbangg)
// @copyright    Copyright (c) 2026 nvbangg (github.com/nvbangg)
// @match        https://github.com/*/*
// @icon         https://github.com/favicon.ico
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(() => {
  "use strict";

  const TAB_ID = "releases-tab-link";

  // Returns true when the current page is the releases section.
  function isReleasesPage() {
    return /\/releases(\/|$)/.test(location.pathname);
  }

  function getCodeAnchor() {
    return document.querySelector(
      'nav[aria-label="Repository"] a[data-tab-item="code"], nav[aria-label="Repository"] #code-tab'
    );
  }

  // Sets or removes aria-current="page" on our tab to match GitHub's
  // own active-tab convention (the only visual difference between tabs).
  // When Releases is active, also strip aria-current from the Code tab so
  // only one tab appears highlighted at a time.
  function updateActiveState() {
    const anchor     = document.getElementById(TAB_ID);
    const codeAnchor = getCodeAnchor();
    if (!anchor) return;

    if (isReleasesPage()) {
      anchor.setAttribute("aria-current", "page");
      // Code tab must not appear active while we're on the releases page.
      if (codeAnchor) codeAnchor.removeAttribute("aria-current");
    } else {
      anchor.removeAttribute("aria-current");
      // Let GitHub's own rendering decide whether Code is active again;
      // we only restore it if it was previously cleared by us and GitHub
      // hasn't already re-set it via Turbo.
      if (codeAnchor && !codeAnchor.hasAttribute("aria-current")) {
        const onCodePage = /^\/[^/]+\/[^/]+(\/tree\/|\/blob\/|\/)?$/.test(location.pathname);
        if (onCodePage) codeAnchor.setAttribute("aria-current", "page");
      }
    }
  }

  function insertReleasesTab() {
    const nav = document.querySelector('nav[aria-label="Repository"]');
    if (!nav || nav.querySelector(`#${TAB_ID}`)) {
      // Tab already exists — just sync the active state.
      updateActiveState();
      return;
    }

    // Bail if releases tab already exists natively.
    if (nav.querySelector('a[href$="/releases"]')) return;

    const codeAnchor = nav.querySelector('a[data-tab-item="code"], #code-tab');
    if (!codeAnchor) return;

    const urlParts = location.pathname.split("/");
    if (urlParts.length < 3) return;
    const releasesHref = `/${urlParts[1]}/${urlParts[2]}/releases`;

    // Read class names live from the Code tab so we match whatever
    // GitHub is currently using — no hardcoded strings that can go stale.
    const liClass = codeAnchor.closest("li")?.className ?? "";
    const aClass  = codeAnchor.className ?? "";

    // Build the SVG using the same attributes as every other tab icon.
    const existingSvg = codeAnchor.querySelector("svg");
    const svgAttrs = existingSvg
      ? `data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-tag" viewBox="0 0 16 16" width="${existingSvg.getAttribute("width") || 16}" height="${existingSvg.getAttribute("height") || 16}" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"`
      : `aria-hidden="true" focusable="false" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"`;

    // aria-current="page" is set here immediately if we are already on the
    // releases page (e.g. hard reload directly onto a releases URL).
    const ariaCurrent = isReleasesPage() ? 'aria-current="page"' : "";

    const html = `
      <li class="${liClass}">
        <a id="${TAB_ID}"
           href="${releasesHref}"
           class="${aClass}"
           ${ariaCurrent}
           data-tab-item="releases"
           data-turbo-frame="repo-content-turbo-frame"
           data-discover="true"
           aria-label="Releases">
          <span data-component="icon">
            <svg ${svgAttrs}>
              <path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
            </svg>
          </span>
          <span data-component="text" data-content="Releases">Releases</span>
        </a>
      </li>`;

    const codeLi = codeAnchor.closest("li") ?? codeAnchor;
    codeLi.insertAdjacentHTML("afterend", html);

    // Guard against GitHub's overflow system hiding our tab.
    const injectedLi     = document.getElementById(TAB_ID)?.closest("li");
    const injectedAnchor = document.getElementById(TAB_ID);
    if (!injectedLi || !injectedAnchor) return;

    new MutationObserver(() => {
      if (injectedLi.getAttribute("aria-hidden") === "true")
        injectedLi.removeAttribute("aria-hidden");
      if (injectedAnchor.getAttribute("tabindex") === "-1")
        injectedAnchor.removeAttribute("tabindex");
    }).observe(injectedLi, { attributes: true });
  }

  // ── SPA navigation ────────────────────────────────────────────────────────
  // insertReleasesTab re-inserts the tab after Turbo swaps the nav out,
  // and also calls updateActiveState() when the tab already exists.
  // popstate covers back/forward without a Turbo event.
  document.addEventListener("turbo:load",      insertReleasesTab);
  document.addEventListener("turbolinks:load", insertReleasesTab);
  window.addEventListener("popstate",          insertReleasesTab);

  insertReleasesTab();
  new MutationObserver(insertReleasesTab).observe(document.documentElement, {
    childList: true,
    subtree: true,
  });
})();