GitHub Repos Enhanced (Grid Layout + README Preview)

Transform GitHub repositories into beautiful grid cards with README preview on hover

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GitHub Repos Enhanced (Grid Layout + README Preview)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Transform GitHub repositories into beautiful grid cards with README preview on hover
// @author       You
// @match        https://github.com/*?tab=repositories*
// @match        https://github.com/*/*
// @icon         https://github.githubassets.com/favicons/favicon.svg
// @require      https://code.jquery.com/jquery-3.6.0.min.js

// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // === GitHub Token 設定(選填,避免 API 限制) ===
    const GITHUB_TOKEN = "";

    // 添加 CSS 樣式
    GM_addStyle(`
        /* Grid Container */
        #repo-grid-container {
            display: grid;
            gap: 20px;
            padding: 20px;
            transition: all 0.3s ease;
        }

        #repo-grid-container.cols-1 { grid-template-columns: repeat(1, 1fr); }
        #repo-grid-container.cols-2 { grid-template-columns: repeat(2, 1fr); }
        #repo-grid-container.cols-3 { grid-template-columns: repeat(3, 1fr); }
        #repo-grid-container.cols-4 { grid-template-columns: repeat(4, 1fr); }

        /* 響應式設計 */
        @media (max-width: 1400px) {
            #repo-grid-container.cols-4 { grid-template-columns: repeat(3, 1fr); }
        }
        @media (max-width: 1024px) {
            #repo-grid-container.cols-4,
            #repo-grid-container.cols-3 { grid-template-columns: repeat(2, 1fr); }
        }
        @media (max-width: 768px) {
            #repo-grid-container { grid-template-columns: repeat(1, 1fr) !important; }
        }

        /* Card 樣式 */
        .repo-card {
            background: #ffffff;
            border: 1px solid #d0d7de;
            border-radius: 12px;
            padding: 20px;
            transition: all 0.3s ease;
            display: flex;
            flex-direction: column;
            height: 100%;
            position: relative;
            overflow: hidden;
        }

        .repo-card:hover {
            transform: translateY(-4px);
            box-shadow: 0 8px 24px rgba(31, 35, 40, 0.15);
            border-color: #0969da;
        }

        .repo-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 3px;
            background: linear-gradient(90deg, #0969da, #1a7f37);
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        .repo-card:hover::before {
            opacity: 1;
        }

        /* 標題 */
        .repo-card-title {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 12px;
            color: #0969da;
            text-decoration: none;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .repo-card-title:hover {
            text-decoration: underline;
        }

        /* 描述 */
        .repo-card-description {
            color: #57606a;
            font-size: 14px;
            margin-bottom: 16px;
            flex-grow: 1;
            line-height: 1.5;
        }

        /* 底部資訊 */
        .repo-card-meta {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            align-items: center;
            font-size: 12px;
            color: #57606a;
            padding-top: 12px;
            border-top: 1px solid #d0d7de;
        }

        .repo-card-language {
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .language-color {
            width: 12px;
            height: 12px;
            border-radius: 50%;
        }

        .repo-card-stars {
            display: flex;
            align-items: center;
            gap: 4px;
        }

        /* Badge 樣式 - 柔和配色 */
        .repo-badge {
            display: inline-block;
            padding: 3px 10px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 500;
            border: 1px solid;
        }

        .repo-badge.public {
            color: #1a7f37;
            border-color: #1a7f37;
            background: #dafbe1;
        }

        .repo-badge.private {
            color: #bf8700;
            border-color: #bf8700;
            background: #fff8c5;
        }

        /* Layout 下拉選單樣式 */
        #grid-layout-dropdown {
            display: inline-block;
            position: relative;
            margin-left: 8px;
        }

        #grid-layout-dropdown summary {
            list-style: none;
            cursor: pointer;
        }

        #grid-layout-dropdown summary::-webkit-details-marker {
            display: none;
        }

        .grid-layout-menu {
            position: absolute;
            top: 100%;
            right: 0;
            margin-top: 4px;
            background: #ffffff;
            border: 1px solid #d0d7de;
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(31, 35, 40, 0.15);
            min-width: 140px;
            z-index: 1000;
        }

        .grid-layout-menu ul {
            padding: 4px;
        }

        .grid-layout-menu-item {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 6px 10px;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.15s ease;
            position: relative;
        }

        .grid-layout-menu-item:hover {
            background: #f6f8fa;
        }

        .grid-layout-menu-item.active {
            background: #ddf4ff;
        }

        .grid-layout-menu-item > svg:first-child {
            width: 14px;
            height: 14px;
            flex-shrink: 0;
            color: #57606a;
        }

        .grid-layout-menu-item.active > svg:first-child {
            color: #0969da;
        }

        .grid-layout-menu-item-text {
            flex: 1;
            font-size: 13px;
            color: #24292f;
            font-weight: 400;
        }

        .grid-layout-menu-item.active .grid-layout-menu-item-text {
            font-weight: 500;
            color: #0969da;
        }

        .grid-layout-menu-item-check {
            width: 14px;
            height: 14px;
            color: #0969da;
            opacity: 0;
            transition: opacity 0.15s ease;
        }

        .grid-layout-menu-item.active .grid-layout-menu-item-check {
            opacity: 1;
        }

        /* 浮動控制面板(備用方案) */
        #grid-control-panel {
            display: none;
            position: fixed;
            bottom: 30px;
            right: 30px;
            background: #ffffff;
            border: 1px solid #d0d7de;
            border-radius: 16px;
            padding: 16px;
            box-shadow: 0 8px 32px rgba(31, 35, 40, 0.15);
            z-index: 10000;
        }

        #grid-control-panel h3 {
            margin: 0 0 12px 0;
            font-size: 14px;
            color: #24292f;
            font-weight: 600;
        }

        .grid-buttons {
            display: flex;
            gap: 8px;
        }

        .grid-btn {
            padding: 8px 16px;
            background: #f6f8fa;
            border: 1px solid #d0d7de;
            border-radius: 8px;
            color: #24292f;
            cursor: pointer;
            transition: all 0.2s ease;
            font-size: 13px;
            font-weight: 500;
        }

        .grid-btn:hover {
            background: #ffffff;
            border-color: #0969da;
            color: #0969da;
        }

        .grid-btn.active {
            background: #0969da;
            border-color: #0969da;
            color: white;
        }

        /* ============ README Preview 樣式 ============ */
        .repo-preview {
            position: absolute;
            z-index: 9999;
            background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
            border-radius: 16px;
            padding: 24px;
            width: 700px;
            max-height: 500px;
            overflow: auto;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15),
                        0 3px 12px rgba(0, 0, 0, 0.08),
                        inset 0 1px 0 rgba(255, 255, 255, 0.9);
            display: none;
            font-size: 13px;
            line-height: 1.7;
            border: 1px solid rgba(0, 0, 0, 0.08);
            opacity: 0;
            transform: translateY(-10px) scale(0.95);
            transition: opacity 0.2s ease, transform 0.2s ease;
            backdrop-filter: blur(10px);
        }

        .repo-preview.show {
            opacity: 1;
            transform: translateY(0) scale(1);
        }

        .repo-preview::-webkit-scrollbar {
            width: 8px;
        }

        .repo-preview::-webkit-scrollbar-track {
            background: rgba(0, 0, 0, 0.05);
            border-radius: 10px;
        }

        .repo-preview::-webkit-scrollbar-thumb {
            background: rgba(0, 0, 0, 0.2);
            border-radius: 10px;
        }

        .repo-preview::-webkit-scrollbar-thumb:hover {
            background: rgba(0, 0, 0, 0.3);
        }

        .repo-preview * {
            transform: scale(0.92);
            transform-origin: left top;
        }

        .repo-preview h1 {
            font-size: 28px;
            margin-top: 0;
            margin-bottom: 16px;
            color: #24292f;
            font-weight: 600;
            border-bottom: 2px solid #e1e4e8;
            padding-bottom: 10px;
        }

        .repo-preview h2 {
            font-size: 22px;
            margin-top: 24px;
            margin-bottom: 12px;
            color: #24292f;
            font-weight: 600;
            border-bottom: 1px solid #e1e4e8;
            padding-bottom: 8px;
        }

        .repo-preview h3 {
            font-size: 18px;
            margin-top: 20px;
            margin-bottom: 10px;
            color: #24292f;
            font-weight: 600;
        }

        .repo-preview p {
            margin: 12px 0;
            color: #57606a;
        }

        .repo-preview pre {
            background: #f6f8fa;
            padding: 16px;
            border-radius: 8px;
            overflow-x: auto;
            border: 1px solid #d0d7de;
            margin: 16px 0;
        }

        .repo-preview code {
            background: #eff1f3;
            padding: 3px 6px;
            border-radius: 6px;
            font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
            font-size: 85%;
            color: #24292f;
        }

        .repo-preview pre code {
            background: transparent;
            padding: 0;
            border-radius: 0;
            font-size: 13px;
        }

        .repo-preview img {
            max-width: 100%;
            height: auto;
            border-radius: 8px;
            margin: 12px 0;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        .repo-preview a {
            color: #0969da;
            text-decoration: none;
        }

        .repo-preview a:hover {
            text-decoration: underline;
        }

        .repo-preview ul, .repo-preview ol {
            margin: 12px 0;
            padding-left: 24px;
        }

        .repo-preview li {
            margin: 6px 0;
            color: #57606a;
        }

        .repo-preview blockquote {
            border-left: 4px solid #d0d7de;
            padding-left: 16px;
            margin: 16px 0;
            color: #57606a;
            font-style: italic;
        }

        .repo-preview table {
            border-collapse: collapse;
            width: 100%;
            margin: 16px 0;
        }

        .repo-preview th, .repo-preview td {
            border: 1px solid #d0d7de;
            padding: 8px 12px;
            text-align: left;
        }

        .repo-preview th {
            background: #f6f8fa;
            font-weight: 600;
        }

        .repo-preview hr {
            border: none;
            border-top: 2px solid #e1e4e8;
            margin: 24px 0;
        }

        .preview-header {
            font-size: 11px;
            color: #6e7781;
            margin-bottom: 12px;
            padding-bottom: 8px;
            border-bottom: 1px solid #e1e4e8;
            font-weight: 500;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
    `);

    // 語言顏色映射
    const languageColors = {
        'JavaScript': '#f1e05a',
        'TypeScript': '#3178c6',
        'Python': '#3572A5',
        'HTML': '#e34c26',
        'CSS': '#563d7c',
        'Vue': '#41b883',
        'Java': '#b07219',
        'C++': '#f34b7d',
        'Go': '#00ADD8',
        'Rust': '#dea584',
        'PHP': '#4F5D95',
        'Ruby': '#701516',
        'Swift': '#ffac45',
        'Kotlin': '#A97BFF',
        'C#': '#178600',
        'Shell': '#89e051',
    };

    function getLanguageColor(lang) {
        return languageColors[lang] || '#8b949e';
    }

    // ============ README Preview 功能 ============
    const preview = document.createElement("div");
    preview.className = "repo-preview";
    document.body.appendChild(preview);

    let hideTimeout;
    let currentLink = null;
    let currentRepo = null;
    let isOverPreview = false;

    async function fetchReadme(owner, repo) {
        const url = `https://api.github.com/repos/${owner}/${repo}/readme`;
        const headers = { "Accept": "application/vnd.github.v3.raw" };
        if (GITHUB_TOKEN) headers["Authorization"] = "token " + GITHUB_TOKEN;

        const res = await fetch(url, { headers });
        if (!res.ok) return "📄 No README found or API limit reached.";
        return res.text();
    }

    function showPreview(x, y, immediate = false) {
        clearTimeout(hideTimeout);

        preview.style.left = (x + 20) + "px";
        preview.style.top = (y + 20) + "px";

        if (preview.style.display === "none") {
            preview.style.display = "block";
            if (immediate) {
                preview.classList.add('show');
            } else {
                setTimeout(() => preview.classList.add('show'), 10);
            }
        }
    }

    function hidePreview(delay = 300) {
        clearTimeout(hideTimeout);
        hideTimeout = setTimeout(() => {
            if (!isOverPreview) {
                preview.classList.remove('show');
                setTimeout(() => {
                    if (!isOverPreview) {
                        preview.style.display = "none";
                        currentLink = null;
                        currentRepo = null;
                    }
                }, 200);
            }
        }, delay);
    }

    document.addEventListener("mouseover", async (e) => {
        const link = e.target.closest("a[itemprop='name codeRepository']");

        if (link) {
            const url = new URL(link.href);
            const [owner, repo] = url.pathname.split("/").filter(Boolean);
            const repoKey = `${owner}/${repo}`;

            if (currentRepo === repoKey) {
                showPreview(e.pageX, e.pageY, true);
                return;
            }

            currentLink = link;
            currentRepo = repoKey;

            showPreview(e.pageX, e.pageY, preview.style.display !== "none");
            preview.innerHTML = `<div class="preview-header">📖 README Preview</div><em style="color: #6e7781;">Loading README...</em>`;

            try {
                const readme = await fetchReadme(owner, repo);
                if (currentRepo === repoKey) {
                    preview.innerHTML = `<div class="preview-header">📖 ${owner}/${repo}</div>` + marked.parse(readme);
                }
            } catch(err) {
                if (currentRepo === repoKey) {
                    preview.innerHTML = `<div class="preview-header">❌ Error</div><span style="color: #cf222e;">Error loading README</span>`;
                }
            }
        }
    });

    document.addEventListener("mouseout", (e) => {
        const link = e.target.closest("a[itemprop='name codeRepository']");
        const relatedTarget = e.relatedTarget;

        if (link && !preview.contains(relatedTarget)) {
            const nextLink = relatedTarget?.closest?.("a[itemprop='name codeRepository']");
            if (!nextLink) {
                hidePreview();
            }
        }
    });

    preview.addEventListener("mouseenter", () => {
        isOverPreview = true;
        clearTimeout(hideTimeout);
    });

    preview.addEventListener("mouseleave", () => {
        isOverPreview = false;
        hidePreview();
    });

    // ============ Grid Layout 功能 ============
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve) => {
            if ($(selector).length) {
                resolve($(selector));
                return;
            }

            const observer = new MutationObserver(() => {
                if ($(selector).length) {
                    observer.disconnect();
                    resolve($(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
                resolve(null);
            }, timeout);
        });
    }

    async function transformRepoList() {
        console.log('🔍 Starting transformation...');
        
        let repoList = await waitForElement('#user-repositories-list');
        
        if (!repoList || repoList.length === 0) {
            repoList = await waitForElement('[data-filterable-for="your-repos-filter"]');
        }
        
        if (!repoList || repoList.length === 0) {
            repoList = $('div[data-hpc] ul').first();
        }
        
        if (!repoList || repoList.length === 0) {
            console.log('❌ Repository list not found');
            return;
        }

        const $gridContainer = $('<div id="repo-grid-container" class="cols-3"></div>');

        let $repos = repoList.find('li');
        
        if ($repos.length === 0) {
            $repos = repoList.children();
        }

        $repos.each(function() {
            const $repo = $(this);

            let $link = $repo.find('a[itemprop="name codeRepository"]');
            if ($link.length === 0) {
                $link = $repo.find('h3 a').first();
            }
            if ($link.length === 0) {
                $link = $repo.find('a').first();
            }
            
            const repoName = $link.text().trim();
            const repoUrl = $link.attr('href');

            if (!repoName) return;

            let description = $repo.find('p[itemprop="description"]').text().trim();
            if (!description) {
                description = $repo.find('p').first().text().trim();
            }
            description = description || '無描述';

            let language = $repo.find('[itemprop="programmingLanguage"]').text().trim();
            if (!language) {
                language = $repo.find('span[class*="color-fg"]').first().text().trim();
            }
            
            const stars = $repo.find('a[href*="/stargazers"]').text().trim();
            const isPublic = $repo.find('span').filter(function() {
                return $(this).text().trim() === 'Public';
            }).length > 0;
            
            let updated = $repo.find('relative-time').attr('datetime');
            if (!updated) {
                updated = $repo.find('relative-time').attr('title') || new Date().toISOString();
            }

            const $card = $(`
                <div class="repo-card">
                    <div>
                        <a href="${repoUrl}" class="repo-card-title" itemprop="name codeRepository">
                            <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor">
                                <path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path>
                            </svg>
                            ${repoName}
                        </a>
                        <span class="repo-badge ${isPublic ? 'public' : 'private'}">
                            ${isPublic ? 'Public' : 'Private'}
                        </span>
                    </div>
                    <div class="repo-card-description">${description}</div>
                    <div class="repo-card-meta">
                        ${language ? `
                            <span class="repo-card-language">
                                <span class="language-color" style="background-color: ${getLanguageColor(language)}"></span>
                                ${language}
                            </span>
                        ` : ''}
                        ${stars ? `
                            <span class="repo-card-stars">
                                <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor">
                                    <path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"></path>
                                </svg>
                                ${stars}
                            </span>
                        ` : ''}
                        <span>Updated ${new Date(updated).toLocaleDateString()}</span>
                    </div>
                </div>
            `);

            $gridContainer.append($card);
        });

        if (repoList.next().length > 0) {
            repoList.after($gridContainer);
        } else if (repoList.parent().length > 0) {
            repoList.parent().append($gridContainer);
        } else {
            repoList.append($gridContainer);
        }

        repoList.hide();
        createGridControls($gridContainer);
    }

    function createGridControls($gridContainer) {
        let $sortContainer = $('summary[aria-haspopup="menu"]').filter(function() {
            return $(this).find('span').text().includes('Sort');
        }).parent();
        
        if ($sortContainer.length === 0) {
            $sortContainer = $('details').filter(function() {
                return $(this).find('summary').text().includes('Sort');
            }).first();
        }
        
        if ($sortContainer.length === 0) {
            createFallbackControls($gridContainer);
            return;
        }

        const $gridDropdown = $(`
            <details id="grid-layout-dropdown" class="details-reset details-overlay">
                <summary class="btn" aria-haspopup="menu" role="button">
                    <span>Layout</span>
                    <span class="dropdown-caret"></span>
                </summary>
                <div class="grid-layout-menu">
                    <ul style="list-style: none; margin: 0; padding: 0;">
                        <li class="grid-layout-menu-item" data-cols="1">
                            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
                                <rect x="3" y="4" width="10" height="2" rx="0.5"/>
                                <rect x="3" y="7" width="10" height="2" rx="0.5"/>
                                <rect x="3" y="10" width="10" height="2" rx="0.5"/>
                            </svg>
                            <span class="grid-layout-menu-item-text">列表</span>
                            <svg class="grid-layout-menu-item-check" viewBox="0 0 16 16" fill="currentColor">
                                <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path>
                            </svg>
                        </li>
                        <li class="grid-layout-menu-item" data-cols="2">
                            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
                                <rect x="3" y="4" width="4.5" height="8" rx="0.5"/>
                                <rect x="8.5" y="4" width="4.5" height="8" rx="0.5"/>
                            </svg>
                            <span class="grid-layout-menu-item-text">兩欄</span>
                            <svg class="grid-layout-menu-item-check" viewBox="0 0 16 16" fill="currentColor">
                                <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path>
                            </svg>
                        </li>
                        <li class="grid-layout-menu-item active" data-cols="3">
                            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
                                <rect x="2.5" y="4" width="3" height="8" rx="0.5"/>
                                <rect x="6.5" y="4" width="3" height="8" rx="0.5"/>
                                <rect x="10.5" y="4" width="3" height="8" rx="0.5"/>
                            </svg>
                            <span class="grid-layout-menu-item-text">三欄</span>
                            <svg class="grid-layout-menu-item-check" viewBox="0 0 16 16" fill="currentColor">
                                <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path>
                            </svg>
                        </li>
                        <li class="grid-layout-menu-item" data-cols="4">
                            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
                                <rect x="2" y="4" width="2.2" height="8" rx="0.5"/>
                                <rect x="5" y="4" width="2.2" height="8" rx="0.5"/>
                                <rect x="8" y="4" width="2.2" height="8" rx="0.5"/>
                                <rect x="11" y="4" width="2.2" height="8" rx="0.5"/>
                            </svg>
                            <span class="grid-layout-menu-item-text">四欄</span>
                            <svg class="grid-layout-menu-item-check" viewBox="0 0 16 16" fill="currentColor">
                                <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path>
                            </svg>
                        </li>
                    </ul>
                </div>
            </details>
        `);

        $sortContainer.after($gridDropdown);

        $('.grid-layout-menu-item').on('click', function(e) {
            e.preventDefault();
            
            const cols = $(this).data('cols');
            $('.grid-layout-menu-item').removeClass('active');
            $(this).addClass('active');
            $gridContainer.attr('class', `cols-${cols}`);
            window.gridLayoutPreference = cols;
            $('#grid-layout-dropdown').removeAttr('open');
        });

        const savedCols = window.gridLayoutPreference || '3';
        $(`.grid-layout-menu-item[data-cols="${savedCols}"]`).addClass('active').siblings().removeClass('active');
        $gridContainer.attr('class', `cols-${savedCols}`);
    }

    function createFallbackControls($gridContainer) {
        const $controlPanel = $(`
            <div id="grid-control-panel" style="display: block !important;">
                <h3>📐 Grid Layout</h3>
                <div class="grid-buttons">
                    <button class="grid-btn" data-cols="1">1 列</button>
                    <button class="grid-btn" data-cols="2">2 列</button>
                    <button class="grid-btn active" data-cols="3">3 列</button>
                    <button class="grid-btn" data-cols="4">4 列</button>
                </div>
            </div>
        `);

        $('body').append($controlPanel);

        $('#grid-control-panel .grid-btn').on('click', function() {
            const cols = $(this).data('cols');
            $('#grid-control-panel .grid-btn').removeClass('active');
            $(this).addClass('active');
            $gridContainer.attr('class', `cols-${cols}`);
            window.gridLayoutPreference = cols;
        });

        const savedCols = window.gridLayoutPreference || '3';
        $(`#grid-control-panel .grid-btn[data-cols="${savedCols}"]`).click();
    }

    // 頁面載入
    $(document).ready(function() {
        console.log('🚀 GitHub Repos Enhanced loaded');
        
        if (window.location.href.includes('?tab=repositories') || 
            window.location.href.includes('&tab=repositories')) {
            setTimeout(() => {
                transformRepoList();
            }, 1000);
        }
    });

    // 監聽 URL 變化
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            if (url.includes('?tab=repositories') || url.includes('&tab=repositories')) {
                setTimeout(() => {
                    transformRepoList();
                }, 1500);
            }
        }
    }).observe(document, {subtree: true, childList: true});
})();