Add GitHub Releases Tab

Add Releases tab after Code tab on GitHub repos

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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,
  });
})();