Better GitHub Navigation

Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.

2026-02-25 기준 버전입니다. 최신 버전을 확인하세요.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Better GitHub Navigation
// @name:zh-CN   更好的 GitHub 导航栏
// @namespace    https://github.com/ImXiangYu/better-github-nav
// @version      0.1.27
// @description  Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.
// @description:zh-CN 在 GitHub 顶部导航中加入 Dashboard、Trending、Explore、Collections、Stars 快捷入口,常用页面一键直达。
// @author       Ayubass
// @license      MIT
// @match        https://github.com/*
// @icon         https://github.githubassets.com/pinned-octocat.svg
// @grant        GM_registerMenuCommand
// ==/UserScript==

(() => {
  // src/constants.js
  var SCRIPT_VERSION = "0.1.27";
  var CUSTOM_BUTTON_CLASS = "custom-gh-nav-btn";
  var CUSTOM_BUTTON_ACTIVE_CLASS = "custom-gh-nav-btn-active";
  var CUSTOM_BUTTON_COMPACT_CLASS = "custom-gh-nav-btn-compact";
  var QUICK_LINK_MARK_ATTR = "data-better-gh-nav-quick-link";
  var CONFIG_STORAGE_KEY = "better-gh-nav-config-v1";
  var UI_LANG_STORAGE_KEY = "better-gh-nav-ui-lang-v1";
  var SETTINGS_OVERLAY_ID = "custom-gh-nav-settings-overlay";
  var SETTINGS_PANEL_ID = "custom-gh-nav-settings-panel";
  var SETTINGS_MESSAGE_ID = "custom-gh-nav-settings-message";
  var DEFAULT_LINK_KEYS = ["dashboard", "explore", "trending", "collections", "stars"];
  var PRESET_LINKS = [
    { key: "dashboard", text: "Dashboard", path: "/dashboard", getHref: () => "/dashboard" },
    { key: "explore", text: "Explore", path: "/explore", getHref: () => "/explore" },
    { key: "trending", text: "Trending", path: "/trending", getHref: () => "/trending" },
    { key: "collections", text: "Collections", path: "/collections", getHref: () => "/collections" },
    { key: "stars", text: "Stars", path: "/stars", getHref: (username) => username ? `/${username}?tab=stars` : "/stars" }
  ];
  var I18N = {
    zh: {
      menuOpenSettings: "Better GitHub Nav: 打开设置面板",
      menuResetSettings: "Better GitHub Nav: 重置快捷链接配置",
      menuLangZh: "Better GitHub Nav: 界面语言 -> 中文",
      menuLangEn: "Better GitHub Nav: 界面语言 -> English",
      menuLangAuto: "Better GitHub Nav: 界面语言 -> 自动(跟随页面)",
      resetConfirm: "确认重置快捷链接配置为默认值吗?",
      panelTitle: "Better GitHub Nav 设置",
      panelDesc: "勾选决定显示项,拖动整行(或右侧手柄)调整显示顺序。",
      resetDefault: "恢复默认",
      cancel: "取消",
      saveAndRefresh: "保存并刷新",
      restoredPendingSave: "已恢复默认,点击保存后生效。",
      atLeastOneLink: "至少保留 1 个快捷链接。",
      dragHandleTitle: "拖动调整顺序",
      dragRowTitle: "拖动整行调整顺序"
    },
    en: {
      menuOpenSettings: "Better GitHub Nav: Open Settings Panel",
      menuResetSettings: "Better GitHub Nav: Reset Quick Link Config",
      menuLangZh: "Better GitHub Nav: UI Language -> 中文",
      menuLangEn: "Better GitHub Nav: UI Language -> English",
      menuLangAuto: "Better GitHub Nav: UI Language -> Auto (Follow Page)",
      resetConfirm: "Reset quick-link config to defaults?",
      panelTitle: "Better GitHub Nav Settings",
      panelDesc: "Select visible links and drag the row (or handle) to reorder.",
      resetDefault: "Reset to Default",
      cancel: "Cancel",
      saveAndRefresh: "Save and Refresh",
      restoredPendingSave: "Defaults restored. Click save to apply.",
      atLeastOneLink: "Keep at least 1 quick link.",
      dragHandleTitle: "Drag to reorder",
      dragRowTitle: "Drag row to reorder"
    }
  };

  // src/config.js
  function sanitizeKeys(keys) {
    const validSet = new Set(DEFAULT_LINK_KEYS);
    const seen = /* @__PURE__ */ new Set();
    const result = [];
    keys.forEach((key) => {
      if (validSet.has(key) && !seen.has(key)) {
        seen.add(key);
        result.push(key);
      }
    });
    return result;
  }
  function sanitizeConfig(rawConfig) {
    const enabledKeys = sanitizeKeys(Array.isArray(rawConfig?.enabledKeys) ? rawConfig.enabledKeys : DEFAULT_LINK_KEYS);
    const orderKeysRaw = sanitizeKeys(Array.isArray(rawConfig?.orderKeys) ? rawConfig.orderKeys : DEFAULT_LINK_KEYS);
    const orderSet = new Set(orderKeysRaw);
    const orderKeys = [
      ...orderKeysRaw,
      ...DEFAULT_LINK_KEYS.filter((key) => !orderSet.has(key))
    ];
    return {
      enabledKeys: enabledKeys.length ? enabledKeys : DEFAULT_LINK_KEYS.slice(),
      orderKeys: orderKeys.length ? orderKeys : DEFAULT_LINK_KEYS.slice()
    };
  }
  function loadConfig() {
    try {
      const raw = localStorage.getItem(CONFIG_STORAGE_KEY);
      if (!raw) return sanitizeConfig({});
      return sanitizeConfig(JSON.parse(raw));
    } catch (e) {
      return sanitizeConfig({});
    }
  }
  function saveConfig(config) {
    localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(sanitizeConfig(config)));
  }
  function getConfiguredLinks(username) {
    const config = loadConfig();
    const presetMap = new Map(
      PRESET_LINKS.map((link) => [link.key, {
        ...link,
        id: `custom-gh-btn-${link.key}`,
        href: link.getHref(username)
      }])
    );
    return config.orderKeys.filter((key) => config.enabledKeys.includes(key)).map((key) => presetMap.get(key)).filter(Boolean);
  }
  function getDisplayNameByKey(key) {
    const link = PRESET_LINKS.find((item) => item.key === key);
    return link ? link.text : key;
  }

  // src/styles.js
  function ensureStyles() {
    if (document.getElementById("custom-gh-nav-style")) return;
    const style = document.createElement("style");
    style.id = "custom-gh-nav-style";
    style.textContent = `
        a.${CUSTOM_BUTTON_CLASS} {
            border-radius: 6px;
            padding-inline: 8px;
            text-decoration: none;
        }
        a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_COMPACT_CLASS} {
            padding-inline: 4px;
        }
        a.${CUSTOM_BUTTON_CLASS},
        a.${CUSTOM_BUTTON_CLASS} span {
            font-weight: 600;
        }
        a.${CUSTOM_BUTTON_CLASS},
        a.${CUSTOM_BUTTON_CLASS} * {
            cursor: pointer;
        }
        a.${CUSTOM_BUTTON_CLASS}:hover {
            background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.12));
            text-decoration: none;
        }
        a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_ACTIVE_CLASS} {
            background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.18));
            font-weight: 600;
        }
        #${SETTINGS_OVERLAY_ID} {
            position: fixed;
            inset: 0;
            z-index: 2147483647;
            background: rgba(0, 0, 0, 0.45);
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 16px;
            box-sizing: border-box;
        }
        #${SETTINGS_PANEL_ID} {
            width: min(560px, 100%);
            max-height: min(80vh, 720px);
            overflow: auto;
            background: var(--color-canvas-default, #fff);
            color: var(--color-fg-default, #1f2328);
            border: 1px solid var(--color-border-default, #d1d9e0);
            border-radius: 10px;
            box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25);
            padding: 16px;
            box-sizing: border-box;
        }
        .custom-gh-nav-settings-title {
            margin: 0 0 8px;
            font-size: 16px;
            line-height: 1.4;
        }
        .custom-gh-nav-settings-desc {
            margin: 0 0 12px;
            color: var(--color-fg-muted, #656d76);
            font-size: 13px;
        }
        .custom-gh-nav-settings-list {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .custom-gh-nav-settings-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            border: 1px solid var(--color-border-muted, #d8dee4);
            border-radius: 8px;
            padding: 8px 10px;
            background: var(--color-canvas-subtle, #f6f8fa);
            cursor: grab;
        }
        .custom-gh-nav-settings-row:active {
            cursor: grabbing;
        }
        .custom-gh-nav-settings-row-left {
            display: inline-flex;
            align-items: center;
            gap: 8px;
            user-select: none;
            font-size: 13px;
        }
        .custom-gh-nav-settings-row-left input {
            cursor: pointer;
        }
        .custom-gh-nav-settings-row-actions {
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }
        .custom-gh-nav-settings-drag-handle {
            border: 1px solid var(--color-border-default, #d1d9e0);
            background: var(--color-btn-bg, #f6f8fa);
            color: var(--color-fg-muted, #656d76);
            border-radius: 6px;
            width: 32px;
            height: 26px;
            line-height: 1;
            font-size: 16px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            user-select: none;
            pointer-events: none;
        }
        .custom-gh-nav-settings-row-dragging {
            opacity: 0.55;
        }
        .custom-gh-nav-settings-row-drag-over {
            border-color: var(--color-accent-fg, #0969da);
            background: var(--color-accent-subtle, #ddf4ff);
        }
        .custom-gh-nav-settings-btn {
            border: 1px solid var(--color-border-default, #d1d9e0);
            background: var(--color-btn-bg, #f6f8fa);
            color: var(--color-fg-default, #1f2328);
            border-radius: 6px;
            padding: 4px 10px;
            font-size: 12px;
            cursor: pointer;
        }
        .custom-gh-nav-settings-btn:hover {
            background: var(--color-btn-hover-bg, #f3f4f6);
        }
        .custom-gh-nav-settings-btn:disabled {
            opacity: 0.45;
            cursor: not-allowed;
        }
        .custom-gh-nav-settings-btn-primary {
            background: var(--color-btn-primary-bg, #1f883d);
            border-color: var(--color-btn-primary-bg, #1f883d);
            color: var(--color-btn-primary-text, #fff);
        }
        .custom-gh-nav-settings-btn-primary:hover {
            background: var(--color-btn-primary-hover-bg, #1a7f37);
        }
        .custom-gh-nav-settings-footer {
            margin-top: 12px;
            display: flex;
            justify-content: flex-end;
            gap: 8px;
        }
        .custom-gh-nav-settings-message {
            min-height: 20px;
            margin-top: 8px;
            color: var(--color-attention-fg, #9a6700);
            font-size: 12px;
        }
    `;
    document.head.appendChild(style);
  }
  function setActiveStyle(aTag, active, compact = false) {
    aTag.classList.add(CUSTOM_BUTTON_CLASS);
    if (compact) {
      aTag.classList.add(CUSTOM_BUTTON_COMPACT_CLASS);
    } else {
      aTag.classList.remove(CUSTOM_BUTTON_COMPACT_CLASS);
    }
    if (active) {
      aTag.setAttribute("aria-current", "page");
      aTag.classList.add(CUSTOM_BUTTON_ACTIVE_CLASS);
    } else {
      aTag.removeAttribute("aria-current");
      aTag.classList.remove(CUSTOM_BUTTON_ACTIVE_CLASS);
    }
  }

  // src/navigation.js
  function normalizePath(href) {
    try {
      const url = new URL(href, location.origin);
      const path = url.pathname.replace(/\/+$/, "");
      return path || "/";
    } catch (e) {
      return "";
    }
  }
  function isCurrentPage(linkPath) {
    const currentPath = location.pathname.replace(/\/+$/, "") || "/";
    if (linkPath === "/dashboard") return currentPath === "/" || currentPath === "/dashboard";
    if (currentPath === linkPath) return true;
    if (linkPath !== "/" && currentPath.startsWith(`${linkPath}/`)) return true;
    return location.search.includes("tab=stars") && linkPath === normalizePath("/stars");
  }
  function setLinkText(aTag, text) {
    const innerSpan = aTag.querySelector("span");
    if (innerSpan) {
      innerSpan.textContent = text;
    } else {
      aTag.textContent = text;
    }
  }
  function ensureAnchor(node, isLiParent) {
    let aTag = isLiParent ? node.querySelector("a") : node.tagName.toLowerCase() === "a" ? node : node.querySelector("a");
    if (aTag) return aTag;
    const fallbackText = (node.textContent || "").trim();
    const fallbackHref = !isLiParent && node.getAttribute && node.getAttribute("href") ? node.getAttribute("href") : `${location.pathname}${location.search}`;
    const classSource = isLiParent ? node.querySelector('[class*="contextCrumb"], [class*="Breadcrumbs-Item"]') : node;
    const spanTemplate = document.querySelector(
      'header a[class*="contextCrumb"] span[class*="contextCrumbLast"]'
    );
    const spanSource = isLiParent ? node.querySelector("span") : node.querySelector("span");
    aTag = document.createElement("a");
    if (classSource && classSource.className) {
      aTag.className = classSource.className.split(/\s+/).filter((cls) => cls && !cls.includes("contextCrumbStatic")).join(" ");
    }
    if (spanSource && spanSource.className) {
      const innerSpan = document.createElement("span");
      innerSpan.className = spanTemplate && spanTemplate.className ? spanTemplate.className : spanSource.className;
      if (fallbackText) innerSpan.textContent = fallbackText;
      aTag.appendChild(innerSpan);
    }
    if (!aTag.getAttribute("href") && fallbackHref) {
      aTag.setAttribute("href", fallbackHref);
    }
    if (!aTag.textContent.trim() && fallbackText) {
      const innerSpan = aTag.querySelector("span");
      if (innerSpan) {
        innerSpan.textContent = fallbackText;
      } else {
        aTag.textContent = fallbackText;
      }
    }
    if (isLiParent) {
      node.textContent = "";
      node.appendChild(aTag);
    } else {
      node.replaceChildren(aTag);
    }
    return aTag;
  }
  function getAnchorHostNode(anchor) {
    if (!anchor || !anchor.parentNode) return anchor;
    return anchor.parentNode.tagName.toLowerCase() === "li" ? anchor.parentNode : anchor;
  }
  function cleanupQuickLinksForContainer(renderParent, keepNode) {
    const quickAnchors = Array.from(
      document.querySelectorAll(
        'header a[id^="custom-gh-btn-"], header a[' + QUICK_LINK_MARK_ATTR + '="1"]'
      )
    );
    quickAnchors.forEach((anchor) => {
      const host = getAnchorHostNode(anchor);
      if (!host || !host.parentNode) return;
      if (host === keepNode) return;
      if (host.parentNode !== renderParent) {
        host.remove();
        return;
      }
      host.remove();
    });
  }
  function addCustomButtons() {
    const userLoginMeta = document.querySelector('meta[name="user-login"]');
    const username = userLoginMeta ? userLoginMeta.getAttribute("content") : "";
    const navPresetLinks = getConfiguredLinks(username);
    if (!navPresetLinks.length) return;
    const primaryLink = navPresetLinks[0];
    const extraLinks = navPresetLinks.slice(1);
    const fixedPages = /* @__PURE__ */ new Set(["/dashboard", "/trending", "/explore", "/collections"]);
    const shortcutPaths = new Set(PRESET_LINKS.map((link) => link.path));
    const compactPages = /* @__PURE__ */ new Set(["/issues", "/pulls", "/repositories"]);
    const isOnPresetPage = Array.from(fixedPages).some((path) => isCurrentPage(path));
    const shouldUseCompactButtons = Array.from(compactPages).some((path) => isCurrentPage(path));
    let targetNode = null;
    let targetSource = "";
    if (isOnPresetPage) {
      targetNode = document.querySelector(
        'header nav a[href="/dashboard"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/trending"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
      );
      if (targetNode) targetSource = "preset-nav";
      if (!targetNode) {
        targetNode = document.querySelector(
          'header nav a[id^="custom-gh-btn-"], header nav a[' + QUICK_LINK_MARK_ATTR + '="1"]'
        );
        if (targetNode) targetSource = "preset-quick";
      }
    } else {
      const breadcrumbNodes = Array.from(document.querySelectorAll(
        'header nav[aria-label*="breadcrumb" i] a[href^="/"], header a[class*="contextCrumb"][href^="/"], header a[class*="Breadcrumbs-Item"][href^="/"]'
      )).filter((link) => {
        if (link.id && link.id.startsWith("custom-gh-btn-")) return false;
        if (link.getAttribute(QUICK_LINK_MARK_ATTR) === "1") return false;
        const href = normalizePath(link.getAttribute("href") || "");
        if (!href || href === "/") return false;
        if (shortcutPaths.has(href)) return false;
        return true;
      });
      if (breadcrumbNodes.length) {
        targetNode = breadcrumbNodes[breadcrumbNodes.length - 1];
        targetSource = "breadcrumb";
      }
    }
    if (!targetNode) {
      targetNode = document.querySelector(
        'header nav a[aria-current="page"]:not([id^="custom-gh-btn-"]), header nav a[data-active="true"]:not([id^="custom-gh-btn-"]), header nav [aria-current="page"]:not(a), header nav [data-active="true"]:not(a)'
      );
      if (targetNode) targetSource = "current-nav";
    }
    if (!targetNode) {
      const navLinks = document.querySelectorAll(
        'header a:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
      );
      for (const link of navLinks) {
        const text = link.textContent.trim().toLowerCase();
        const href = link.getAttribute("href");
        if (text === "dashboard" || href === "/dashboard") {
          targetNode = link;
          targetSource = "legacy-dashboard";
          break;
        }
      }
    }
    if (!targetNode) {
      const currentPath = location.pathname.replace(/\/+$/, "") || "/";
      const globalNavCandidates = Array.from(
        document.querySelectorAll(
          'header nav[aria-label*="global" i] a[href^="/"], header nav[aria-label*="header" i] a[href^="/"], header nav a[href="/pulls"], header nav a[href="/issues"], header nav a[href="/repositories"], header nav a[href="/codespaces"], header nav a[href="/marketplace"], header nav a[href="/explore"]'
        )
      ).filter((link) => {
        const href = normalizePath(link.getAttribute("href") || "");
        if (!href || href === "/") return false;
        if (link.id && link.id.startsWith("custom-gh-btn-")) return false;
        if (link.getAttribute(QUICK_LINK_MARK_ATTR) === "1") return false;
        return true;
      });
      if (globalNavCandidates.length) {
        targetNode = globalNavCandidates.find((link) => {
          const href = normalizePath(link.getAttribute("href") || "");
          return href === currentPath;
        }) || globalNavCandidates[globalNavCandidates.length - 1];
        if (targetNode) targetSource = "global-nav";
      }
    }
    if (!targetNode) {
      const currentTextNode = document.querySelector(
        'header nav [aria-current="page"]:not(a), header nav [data-active="true"]:not(a)'
      );
      if (currentTextNode) {
        targetNode = currentTextNode;
        targetSource = "current-text";
      }
    }
    if (!targetNode) {
      const contextCrumbTextNodes = document.querySelectorAll(
        'header span[class*="contextCrumbStatic"], header span[class*="contextCrumb"][class*="Breadcrumbs-Item"], header .prc-Breadcrumbs-Item-jcraJ'
      );
      if (contextCrumbTextNodes.length) {
        targetNode = contextCrumbTextNodes[contextCrumbTextNodes.length - 1];
        targetSource = "crumb-text";
      }
    }
    let templateNode = targetNode;
    if (targetNode) {
      const localNav = targetNode.closest("nav, ul, ol");
      const localAnchors = localNav ? localNav.querySelectorAll(
        'a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
      ) : [];
      if (localAnchors.length) {
        templateNode = localAnchors[localAnchors.length - 1];
      } else {
        const nativeNavAnchors = document.querySelectorAll(
          'header nav[aria-label*="breadcrumb" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header a[class*="contextCrumb"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header a[class*="Breadcrumbs-Item"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav[aria-label*="global" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav[aria-label*="header" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/pulls"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/issues"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/repositories"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/codespaces"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/marketplace"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
        );
        if (nativeNavAnchors.length) {
          templateNode = nativeNavAnchors[nativeNavAnchors.length - 1];
        }
      }
    }
    if (targetNode) {
      const isTargetLiParent = targetNode.parentNode.tagName.toLowerCase() === "li";
      const insertAnchorNode = isTargetLiParent ? targetNode.parentNode : targetNode;
      const isTemplateLiParent = templateNode.parentNode.tagName.toLowerCase() === "li";
      const cloneTemplateNode = isTemplateLiParent ? templateNode.parentNode : templateNode;
      const targetHasAnchor = isTargetLiParent ? Boolean(insertAnchorNode.querySelector("a")) : insertAnchorNode.tagName.toLowerCase() === "a" || Boolean(insertAnchorNode.querySelector("a"));
      const shouldForceCreateAnchor = !targetHasAnchor && Boolean(targetNode.closest("header nav"));
      const anchorTag = targetHasAnchor || shouldForceCreateAnchor ? ensureAnchor(insertAnchorNode, isTargetLiParent) : null;
      cleanupQuickLinksForContainer(insertAnchorNode.parentNode, insertAnchorNode);
      const hasShortcutActive = navPresetLinks.some((link) => isCurrentPage(link.path));
      if (isOnPresetPage && anchorTag && primaryLink) {
        anchorTag.id = primaryLink.id;
        anchorTag.setAttribute(QUICK_LINK_MARK_ATTR, "1");
        anchorTag.href = primaryLink.href;
        setLinkText(anchorTag, primaryLink.text);
        setActiveStyle(anchorTag, isCurrentPage(primaryLink.path), shouldUseCompactButtons);
      } else {
        if (anchorTag && anchorTag.id && anchorTag.id.startsWith("custom-gh-btn-")) {
          anchorTag.removeAttribute("id");
        }
        if (anchorTag) {
          anchorTag.removeAttribute(QUICK_LINK_MARK_ATTR);
        }
        if (anchorTag) {
          setActiveStyle(anchorTag, !hasShortcutActive, shouldUseCompactButtons);
        }
      }
      let insertAfterNode = insertAnchorNode;
      const linksToRender = isOnPresetPage ? extraLinks : navPresetLinks;
      linksToRender.forEach((linkInfo) => {
        const newNode = cloneTemplateNode.cloneNode(true);
        const aTag = ensureAnchor(newNode, isTemplateLiParent);
        aTag.id = linkInfo.id;
        aTag.setAttribute(QUICK_LINK_MARK_ATTR, "1");
        aTag.href = linkInfo.href;
        setLinkText(aTag, linkInfo.text);
        setActiveStyle(aTag, isCurrentPage(linkInfo.path), shouldUseCompactButtons);
        insertAfterNode.parentNode.insertBefore(newNode, insertAfterNode.nextSibling);
        insertAfterNode = newNode;
      });
    }
  }

  // src/i18n.js
  var uiLang = detectUiLang();
  function t(key, vars = {}) {
    const dict = I18N[uiLang] || I18N.en;
    const fallback = I18N.en;
    const template = dict[key] || fallback[key] || key;
    return template.replace(/\{(\w+)\}/g, (_, varName) => String(vars[varName] ?? ""));
  }
  function detectUiLang() {
    try {
      const preferredLang = (localStorage.getItem(UI_LANG_STORAGE_KEY) || "").toLowerCase();
      if (preferredLang === "zh" || preferredLang === "en") return preferredLang;
    } catch (e) {
    }
    const autoLang = (document.documentElement.lang || navigator.language || "").toLowerCase();
    return autoLang.startsWith("zh") ? "zh" : "en";
  }
  function setUiLangPreference(lang) {
    try {
      if (lang === "zh" || lang === "en") {
        localStorage.setItem(UI_LANG_STORAGE_KEY, lang);
      } else {
        localStorage.removeItem(UI_LANG_STORAGE_KEY);
      }
    } catch (e) {
    }
    uiLang = detectUiLang();
  }

  // src/settings-panel.js
  var settingsEscHandler = null;
  function closeConfigPanel() {
    const overlay = document.getElementById(SETTINGS_OVERLAY_ID);
    if (overlay) overlay.remove();
    if (settingsEscHandler) {
      document.removeEventListener("keydown", settingsEscHandler);
      settingsEscHandler = null;
    }
  }
  function createPanelState(config) {
    const safeConfig = sanitizeConfig(config);
    return {
      order: safeConfig.orderKeys.slice(),
      enabledSet: new Set(safeConfig.enabledKeys)
    };
  }
  function reorderKeys(state, draggedKey, targetKey, placeAfter = false) {
    const fromIndex = state.order.indexOf(draggedKey);
    const targetIndex = state.order.indexOf(targetKey);
    if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) return false;
    const [movedKey] = state.order.splice(fromIndex, 1);
    let insertIndex = targetIndex + (placeAfter ? 1 : 0);
    if (fromIndex < targetIndex) {
      insertIndex -= 1;
    }
    state.order.splice(insertIndex, 0, movedKey);
    return true;
  }
  function clearDragClasses(listEl) {
    const rows = listEl.querySelectorAll(".custom-gh-nav-settings-row");
    rows.forEach((row) => {
      row.classList.remove("custom-gh-nav-settings-row-dragging");
      row.classList.remove("custom-gh-nav-settings-row-drag-over");
    });
  }
  function renderPanelRows(listEl, state) {
    listEl.replaceChildren();
    state.order.forEach((key) => {
      const row = document.createElement("div");
      row.className = "custom-gh-nav-settings-row";
      row.draggable = true;
      row.title = t("dragRowTitle");
      row.dataset.rowKey = key;
      const left = document.createElement("label");
      left.className = "custom-gh-nav-settings-row-left";
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.checked = state.enabledSet.has(key);
      checkbox.addEventListener("change", () => {
        if (checkbox.checked) {
          state.enabledSet.add(key);
        } else {
          state.enabledSet.delete(key);
        }
      });
      const text = document.createElement("span");
      text.textContent = `${getDisplayNameByKey(key)} (${key})`;
      left.appendChild(checkbox);
      left.appendChild(text);
      const actions = document.createElement("div");
      actions.className = "custom-gh-nav-settings-row-actions";
      const dragHandle = document.createElement("span");
      dragHandle.className = "custom-gh-nav-settings-drag-handle";
      dragHandle.textContent = "≡";
      dragHandle.title = t("dragHandleTitle");
      dragHandle.setAttribute("aria-hidden", "true");
      row.addEventListener("dragstart", (event) => {
        row.classList.add("custom-gh-nav-settings-row-dragging");
        listEl.dataset.dragKey = key;
        if (event.dataTransfer) {
          event.dataTransfer.effectAllowed = "move";
          event.dataTransfer.setData("text/plain", key);
        }
      });
      row.addEventListener("dragend", () => {
        delete listEl.dataset.dragKey;
        clearDragClasses(listEl);
      });
      row.addEventListener("dragover", (event) => {
        event.preventDefault();
        row.classList.add("custom-gh-nav-settings-row-drag-over");
        if (event.dataTransfer) {
          event.dataTransfer.dropEffect = "move";
        }
      });
      row.addEventListener("dragleave", () => {
        row.classList.remove("custom-gh-nav-settings-row-drag-over");
      });
      row.addEventListener("drop", (event) => {
        event.preventDefault();
        row.classList.remove("custom-gh-nav-settings-row-drag-over");
        const draggedKey = event.dataTransfer && event.dataTransfer.getData("text/plain") || listEl.dataset.dragKey || "";
        if (!draggedKey || draggedKey === key) return;
        const rect = row.getBoundingClientRect();
        const placeAfter = event.clientY > rect.top + rect.height / 2;
        if (reorderKeys(state, draggedKey, key, placeAfter)) {
          renderPanelRows(listEl, state);
        }
      });
      actions.appendChild(dragHandle);
      row.appendChild(left);
      row.appendChild(actions);
      listEl.appendChild(row);
    });
  }
  function openConfigPanel() {
    closeConfigPanel();
    ensureStyles();
    const state = createPanelState(loadConfig());
    const overlay = document.createElement("div");
    overlay.id = SETTINGS_OVERLAY_ID;
    const panel = document.createElement("div");
    panel.id = SETTINGS_PANEL_ID;
    const title = document.createElement("h3");
    title.className = "custom-gh-nav-settings-title";
    title.textContent = t("panelTitle");
    const desc = document.createElement("p");
    desc.className = "custom-gh-nav-settings-desc";
    desc.textContent = t("panelDesc");
    const list = document.createElement("div");
    list.className = "custom-gh-nav-settings-list";
    renderPanelRows(list, state);
    const message = document.createElement("div");
    message.id = SETTINGS_MESSAGE_ID;
    message.className = "custom-gh-nav-settings-message";
    message.setAttribute("role", "status");
    message.setAttribute("aria-live", "polite");
    const footer = document.createElement("div");
    footer.className = "custom-gh-nav-settings-footer";
    const resetBtn = document.createElement("button");
    resetBtn.type = "button";
    resetBtn.className = "custom-gh-nav-settings-btn";
    resetBtn.textContent = t("resetDefault");
    resetBtn.addEventListener("click", () => {
      state.order = DEFAULT_LINK_KEYS.slice();
      state.enabledSet = new Set(DEFAULT_LINK_KEYS);
      renderPanelRows(list, state);
      message.textContent = t("restoredPendingSave");
    });
    const cancelBtn = document.createElement("button");
    cancelBtn.type = "button";
    cancelBtn.className = "custom-gh-nav-settings-btn";
    cancelBtn.textContent = t("cancel");
    cancelBtn.addEventListener("click", closeConfigPanel);
    const saveBtn = document.createElement("button");
    saveBtn.type = "button";
    saveBtn.className = "custom-gh-nav-settings-btn custom-gh-nav-settings-btn-primary";
    saveBtn.textContent = t("saveAndRefresh");
    saveBtn.addEventListener("click", () => {
      const enabledKeys = state.order.filter((key) => state.enabledSet.has(key));
      if (!enabledKeys.length) {
        message.textContent = t("atLeastOneLink");
        return;
      }
      saveConfig({
        enabledKeys,
        orderKeys: state.order.slice()
      });
      closeConfigPanel();
      location.reload();
    });
    footer.appendChild(resetBtn);
    footer.appendChild(cancelBtn);
    footer.appendChild(saveBtn);
    panel.appendChild(title);
    panel.appendChild(desc);
    panel.appendChild(list);
    panel.appendChild(message);
    panel.appendChild(footer);
    overlay.appendChild(panel);
    overlay.addEventListener("click", (event) => {
      if (event.target === overlay) closeConfigPanel();
    });
    settingsEscHandler = (event) => {
      if (event.key === "Escape") closeConfigPanel();
    };
    document.addEventListener("keydown", settingsEscHandler);
    document.body.appendChild(overlay);
  }
  function registerConfigMenu() {
    if (typeof GM_registerMenuCommand !== "function") return;
    GM_registerMenuCommand(t("menuOpenSettings"), openConfigPanel);
    GM_registerMenuCommand(t("menuResetSettings"), () => {
      const shouldReset = confirm(t("resetConfirm"));
      if (!shouldReset) return;
      localStorage.removeItem(CONFIG_STORAGE_KEY);
      closeConfigPanel();
      location.reload();
    });
    GM_registerMenuCommand(t("menuLangZh"), () => {
      setUiLangPreference("zh");
      closeConfigPanel();
      location.reload();
    });
    GM_registerMenuCommand(t("menuLangEn"), () => {
      setUiLangPreference("en");
      closeConfigPanel();
      location.reload();
    });
    GM_registerMenuCommand(t("menuLangAuto"), () => {
      setUiLangPreference("auto");
      closeConfigPanel();
      location.reload();
    });
  }

  // src/main.js
  console.info(`[Better GitHub Navigation] loaded v${SCRIPT_VERSION}`);
  window.__betterGithubNavVersion = SCRIPT_VERSION;
  window.__openBetterGithubNavSettings = openConfigPanel;
  registerConfigMenu();
  ensureStyles();
  addCustomButtons();
  document.addEventListener("turbo:load", addCustomButtons);
  document.addEventListener("pjax:end", addCustomButtons);
  var observer = new MutationObserver(() => {
    if (!document.querySelector('[id^="custom-gh-btn-"]') && document.querySelector("header")) {
      addCustomButtons();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();