Angular Route Switcher

Automatically detects Angular routes and provides a floating UI to switch between them. Works only in Dev Mode (requires window.ng).

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Angular Route Switcher
// @namespace    https://github.com/junyou1998/angular-route-switcher
// @version      1.4.7
// @description      Automatically detects Angular routes and provides a floating UI to switch between them. Works only in Dev Mode (requires window.ng).
// @description:zh-TW 自動偵測 Angular 路由並提供浮動介面進行切換。僅適用於開發模式 (需要 window.ng)。
// @author       junyou
// @match        http://localhost:*/*
// @match        http://127.0.0.1:*/*
// @grant        none
// @icon         https://cdn.jsdelivr.net/gh/junyou1998/angular-route-switcher@main/icon.png
// @license      MIT
// @homepageURL  https://github.com/junyou1998/angular-route-switcher
// @supportURL   https://github.com/junyou1998/angular-route-switcher/issues
// @run-at       document-idle
// @noframes
// ==/UserScript==

(function () {
    "use strict";

    // Configuration
    const CONFIG = {
        pollInterval: 1000,
        maxPollAttempts: 60,
        uiZIndex: 99999,
        colors: {
            primary: "#006D50",
            primaryHover: "#004D38",
            primaryLight: "rgba(0, 109, 80, 0.08)",
            background: "#ffffff",
            text: "#333333",
            border: "#e0e0e0",
            scrollThumb: "#cccccc",
            scrollThumbHover: "#aaaaaa",
            scrollTrack: "#f5f5f5",
        },
    };

    // Localization
    const I18N = {
        en: {
            title: "Available Routes",
            refresh: "↻ Refresh",
            searchPlaceholder: "Search routes...",
            noRoutes: "No routes found",
            dynamicTitle: "(Dynamic Title)",
            paramPrompt: "Route contains parameters. Please edit:",
            tooltip: "Angular Routes",
        },
        "zh-TW": {
            title: "可用路由",
            refresh: "↻ 重新整理",
            searchPlaceholder: "搜尋路由...",
            noRoutes: "未找到路由",
            dynamicTitle: "(動態標題)",
            paramPrompt: "此路由包含參數,請編輯路徑:",
            tooltip: "Angular 路由切換",
        },
    };

    // Detect Language
    let currentLang = "en";
    const languages = navigator.languages || [
        navigator.language || navigator.userLanguage,
    ];

    for (const l of languages) {
        if (!l) continue;
        const lower = l.toLowerCase();

        if (lower.startsWith("zh")) {
            if (
                lower.includes("tw") ||
                lower.includes("hk") ||
                lower === "zh"
            ) {
                currentLang = "zh-TW";
                break;
            }
        } else if (lower.startsWith("en")) {
            currentLang = "en";
            break;
        }
    }

    const TEXT = I18N[currentLang] || I18N["en"];

    let pollAttempts = 0;
    let routes = [];
    let router = null;

    // --- Core Logic ---

    function extractRoutes(config, parentPath = "") {
        let extracted = [];
        for (const route of config) {
            if (route.redirectTo) continue;

            let currentPath = parentPath;
            if (route.path && route.path !== "") {
                currentPath = parentPath
                    ? `${parentPath}/${route.path}`
                    : route.path;
            }

            if (route.path !== "**" && !currentPath.includes("**")) {
                if (
                    route.component ||
                    route.loadComponent ||
                    (route.children && route.children.length > 0)
                ) {
                    let title = route.title;
                    if (!title && route.data && route.data.title) {
                        title = route.data.title;
                    }

                    if (typeof title === "function") {
                        title = TEXT.dynamicTitle;
                    }

                    extracted.push({
                        path: currentPath,
                        title: title || "",
                    });
                }
            }

            if (route.children) {
                extracted = extracted.concat(
                    extractRoutes(route.children, currentPath)
                );
            }
        }

        const unique = new Map();
        extracted.forEach((item) => {
            if (!unique.has(item.path)) {
                unique.set(item.path, item);
            }
        });

        return Array.from(unique.values());
    }

    // Helper: Check if a route path definition matches the current URL
    function isRouteMatch(routeDefinition, currentUrl) {
        // Remove leading slash for consistency
        const def = routeDefinition.startsWith("/")
            ? routeDefinition.slice(1)
            : routeDefinition;
        const url = currentUrl.split("?")[0].startsWith("/")
            ? currentUrl.split("?")[0].slice(1)
            : currentUrl.split("?")[0];

        if (def === url) return true;

        const defSegments = def.split("/");
        const urlSegments = url.split("/");

        if (defSegments.length !== urlSegments.length) return false;

        for (let i = 0; i < defSegments.length; i++) {
            const defSeg = defSegments[i];
            const urlSeg = urlSegments[i];

            // If segment starts with ':', it's a parameter, so it matches anything non-empty
            if (defSeg.startsWith(":")) {
                if (!urlSeg) return false; // Parameter cannot be empty? actually url split won't give empty unless //
                continue;
            }

            if (defSeg !== urlSeg) return false;
        }

        return true;
    }

    function initAngularContext() {
        const rootElement = document.querySelector("[ng-version]");
        if (!rootElement) return false;

        const ng = window.ng;
        if (!ng || !ng.getComponent) return false;

        try {
            let rootComp = ng.getComponent(rootElement);

            if (!rootComp) {
                const allElements = document.querySelectorAll("*");
                for (let i = 0; i < Math.min(allElements.length, 100); i++) {
                    const comp = ng.getComponent(allElements[i]);
                    if (comp) {
                        rootComp = comp;
                        break;
                    }
                }
            }

            if (rootComp) {
                for (const key in rootComp) {
                    if (
                        rootComp[key] &&
                        rootComp[key].config &&
                        typeof rootComp[key].navigateByUrl === "function"
                    ) {
                        router = rootComp[key];
                        break;
                    }
                }
            }

            if (!router) {
                const allElements = document.querySelectorAll("*");
                for (let i = 0; i < Math.min(allElements.length, 500); i++) {
                    const el = allElements[i];
                    const comp = ng.getComponent(el);
                    if (comp) {
                        for (const key in comp) {
                            if (
                                comp[key] &&
                                comp[key].config &&
                                typeof comp[key].navigateByUrl === "function"
                            ) {
                                router = comp[key];
                                break;
                            }
                        }
                    }
                    if (router) break;
                }
            }

            if (router) {
                routes = extractRoutes(router.config);
                createUI();
                return true;
            } else {
                return false;
            }
        } catch (e) {
            return false;
        }
    }

    // --- UI Implementation ---

    function createUI() {
        if (document.getElementById("ng-route-switcher-root")) return;

        const link = document.createElement("link");
        link.rel = "stylesheet";
        link.href =
            "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0";
        document.head.appendChild(link);

        const container = document.createElement("div");
        container.id = "ng-route-switcher-root";

        const shadow = container.attachShadow({ mode: "open" });

        const style = document.createElement("style");
        style.textContent = `
            @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0');
            
            .backdrop {
                position: fixed;
                top: 0;
                left: 0;
                width: 100vw;
                height: 100vh;
                z-index: ${CONFIG.uiZIndex - 2};
                display: none;
            }

            .fab {
                position: fixed;
                width: 50px;
                height: 50px;
                background-color: ${CONFIG.colors.primary};
                border-radius: 50%;
                box-shadow: 0 4px 12px rgba(0,0,0,0.3);
                cursor: grab;
                display: flex;
                align-items: center;
                justify-content: center;
                z-index: ${CONFIG.uiZIndex};
                transition: background-color 0.2s, transform 0.1s, width 0.3s, height 0.3s, border-radius 0.3s;
                color: white;
                user-select: none;
                touch-action: none;
            }
            .fab.snapping {
                transition: transform 0.1s, top 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28), left 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28), background-color 0.2s;
            }
            .fab:active {
                cursor: grabbing;
                transform: scale(0.95);
            }
            .fab:hover {
                background-color: ${CONFIG.colors.primaryHover};
            }
            .fab:hover .minimize-btn {
                opacity: 1;
            }

            /* Minimized (Pill) State */
            .fab.minimized {
                width: 12px;
                height: 48px;
                border-radius: 6px;
                cursor: pointer;
                background-color: ${
                    CONFIG.colors.primary
                }CC; /* Slight transparency */
                overflow: hidden; /* Hide internal elements when small */
            }
            .fab.minimized:hover {
                width: 16px;
                background-color: ${CONFIG.colors.primary};
            }
            .fab.minimized .material-symbols-outlined, 
            .fab.minimized .minimize-btn {
                display: none;
            }

            .minimize-btn {
                position: absolute;
                top: -8px;
                right: -8px;
                width: 24px;
                height: 24px;
                background: rgba(0, 0, 0, 0.4);
                color: white;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                opacity: 0;
                transition: opacity 0.2s, transform 0.2s, background-color 0.2s;
                z-index: 1;
                font-size: 14px;
                box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            }
            .minimize-btn:hover {
                transform: scale(1.1);
                background: rgba(0, 0, 0, 0.7);
            }
            .minimize-btn .material-symbols-outlined {
                font-size: 16px;
                font-weight: bold;
            }

            .material-symbols-outlined {
                font-family: 'Material Symbols Outlined';
                font-weight: normal;
                font-style: normal;
                font-size: 24px;
                line-height: 1;
                letter-spacing: normal;
                text-transform: none;
                display: inline-block;
                white-space: nowrap;
                word-wrap: normal;
                direction: ltr;
            }
            
            /* ... (Rest of existing styles) ... */
            .menu {
                position: fixed;
                background: ${CONFIG.colors.background};
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0,0,0,0.2);
                width: 300px;
                max-height: 400px; 
                z-index: ${CONFIG.uiZIndex - 1};
                display: none;
                flex-direction: column;
                font-family: sans-serif;
                font-size: 14px;
                border: 1px solid ${CONFIG.colors.border};
                overflow: hidden; 
            }
            .menu.open {
                display: flex;
            }
            .menu-header {
                padding: 12px;
                background: #f5f5f5;
                font-weight: bold;
                border-bottom: 1px solid ${CONFIG.colors.border};
                display: flex;
                justify-content: space-between;
                align-items: center;
                color: ${CONFIG.colors.text};
                flex-shrink: 0;
            }
            .search-box {
                padding: 8px;
                border-bottom: 1px solid ${CONFIG.colors.border};
                flex-shrink: 0;
            }
            .search-box input {
                width: 100%;
                padding: 6px;
                box-sizing: border-box;
                border: 1px solid #ddd;
                border-radius: 4px;
                outline: none;
            }
            .search-box input:focus {
                border-color: ${CONFIG.colors.primary};
                box-shadow: 0 0 0 1px ${CONFIG.colors.primary};
            }
            .route-list {
                list-style: none;
                padding: 0;
                margin: 0;
                overflow-y: auto; 
                flex: 1; 
            }
            .route-list::-webkit-scrollbar {
                width: 6px;
                height: 6px;
            }
            .route-list::-webkit-scrollbar-track {
                background: ${CONFIG.colors.scrollTrack};
            }
            .route-list::-webkit-scrollbar-thumb {
                background: ${CONFIG.colors.scrollThumb};
                border-radius: 3px;
            }
            .route-list::-webkit-scrollbar-thumb:hover {
                background: ${CONFIG.colors.scrollThumbHover};
            }

            .route-item {
                padding: 10px 12px;
                cursor: pointer;
                color: ${CONFIG.colors.text};
                border-bottom: 1px solid #f0f0f0;
                transition: background 0.1s;
            }
            .route-item:hover {
                background-color: ${CONFIG.colors.primaryLight};
                color: ${CONFIG.colors.primary};
            }
            .route-item:last-child {
                border-bottom: none;
            }
            .route-path {
                font-weight: 500;
            }
            .route-title {
                font-size: 0.85em;
                color: #666;
                margin-top: 2px;
            }
            .refresh-btn {
                font-size: 12px;
                color: ${CONFIG.colors.primary};
                cursor: pointer;
                background: none;
                border: none;
                padding: 0;
            }
            .route-item {
                position: relative; /* For copy button positioning */
            }
            .route-item.active {
                background-color: ${CONFIG.colors.primaryLight};
                color: ${CONFIG.colors.primary};
                font-weight: bold;
                border-left: 4px solid ${CONFIG.colors.primary};
            }
            .route-item:focus {
                outline: none;
                background-color: ${CONFIG.colors.primaryLight};
                border-left: 4px solid ${CONFIG.colors.primary};
            }
            .copy-btn {
                position: absolute;
                right: 10px;
                top: 50%;
                transform: translateY(-50%);
                background: none;
                border: none;
                cursor: pointer;
                color: #aaa;
                display: none; /* Show on hover */
                padding: 4px;
                border-radius: 4px;
                transition: color 0.2s, background 0.2s;
            }
            .route-item:hover .copy-btn {
                display: flex;
            }
            .copy-btn:hover {
                color: ${CONFIG.colors.primary};
                background-color: rgba(0,0,0,0.05);
            }
        `;

        const backdrop = document.createElement("div");
        backdrop.className = "backdrop";
        backdrop.onclick = () => {
            if (isOpen) toggleMenu();
        };

        const fab = document.createElement("div");
        fab.className = "fab";
        fab.innerHTML =
            '<span class="material-symbols-outlined">explore</span>';
        fab.title = TEXT.tooltip;

        // Minimize Button
        const minimizeBtn = document.createElement("div");
        minimizeBtn.className = "minimize-btn";
        minimizeBtn.innerHTML =
            '<span class="material-symbols-outlined">close</span>';
        minimizeBtn.title = "Minimize";
        fab.appendChild(minimizeBtn);

        let storedState = null;
        try {
            storedState = JSON.parse(localStorage.getItem("ARS_STATE"));
        } catch (e) {}

        let initialTop = window.innerHeight - 80;
        let initialLeft = window.innerWidth - 80;
        let isMinimized = false;
        let dockedSide = "right";

        if (storedState) {
            initialTop =
                storedState.top !== undefined ? storedState.top : initialTop;
            initialLeft =
                storedState.left !== undefined ? storedState.left : initialLeft;
            isMinimized = !!storedState.isMinimized;
            dockedSide = storedState.dockedSide || "right";
        }

        // Validate bounds
        if (initialTop < 20) initialTop = 20;
        if (initialTop > window.innerHeight - 70)
            initialTop = window.innerHeight - 70;
        if (initialLeft < 0) initialLeft = 0;
        if (initialLeft > window.innerWidth - 12)
            initialLeft = window.innerWidth - 50;

        fab.style.top = `${initialTop}px`;
        fab.style.left = `${initialLeft}px`;

        if (isMinimized) {
            fab.classList.add("minimized");
            // small offset check for minimized
            if (dockedSide === "right") {
                fab.style.left = `${window.innerWidth - 12}px`;
            } else {
                fab.style.left = "0px";
            }
        }

        const menu = document.createElement("div");
        menu.className = "menu";

        const header = document.createElement("div");
        header.className = "menu-header";

        const titleSpan = document.createElement("span");
        titleSpan.textContent = TEXT.title;

        const refreshBtn = document.createElement("button");
        refreshBtn.className = "refresh-btn";
        refreshBtn.textContent = TEXT.refresh;
        refreshBtn.onclick = (e) => {
            e.stopPropagation();
            refreshRoutes();
        };

        header.appendChild(titleSpan);
        header.appendChild(refreshBtn);

        const searchBox = document.createElement("div");
        searchBox.className = "search-box";
        const searchInput = document.createElement("input");
        searchInput.placeholder = TEXT.searchPlaceholder;
        searchBox.appendChild(searchInput);

        const ul = document.createElement("ul");
        ul.className = "route-list";

        menu.appendChild(header);
        menu.appendChild(searchBox);
        menu.appendChild(ul);

        shadow.appendChild(style);
        shadow.appendChild(backdrop);
        shadow.appendChild(fab);
        shadow.appendChild(menu);
        document.body.appendChild(container);

        let isOpen = false;

        // --- Persistence Helper ---
        function saveState() {
            const state = {
                top: parseFloat(fab.style.top),
                left: parseFloat(fab.style.left),
                isMinimized: isMinimized,
                dockedSide: dockedSide,
            };
            localStorage.setItem("ARS_STATE", JSON.stringify(state));
        }

        // --- Drag & Drop Logic ---
        let isDragging = false;
        let startX, startY, initialFabLeft, initialFabTop;
        let dragThreshold = 5;
        let hasMoved = false;

        // Track which side the FAB is docked to ('left' or 'right')
        fab.addEventListener("mousedown", dragStart);
        document.addEventListener("mousemove", drag);
        document.addEventListener("mouseup", dragEnd);

        fab.addEventListener("touchstart", dragStart, { passive: false });
        document.addEventListener("touchmove", drag, { passive: false });
        document.addEventListener("touchend", dragEnd);

        // --- Minimize Logic ---
        // Prevent drag from starting on the minimize button
        minimizeBtn.addEventListener("mousedown", (e) => e.stopPropagation());
        minimizeBtn.addEventListener("touchstart", (e) => e.stopPropagation(), {
            passive: true,
        });

        // Handle click
        minimizeBtn.onclick = (e) => {
            e.stopPropagation();
            toggleMinimize();
        };

        function toggleMinimize() {
            if (isOpen) toggleMenu(); // Close menu if open

            isMinimized = !isMinimized;

            if (isMinimized) {
                fab.classList.add("minimized");
                // Snap to very edge
                snapToEdge(true);
            } else {
                fab.classList.remove("minimized");
                snapToEdge();
            }
            saveState(); // Save state AFTER snapping to new position
        }

        // --- Resize Logic ---
        let resizeTimeout;
        window.addEventListener("resize", () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(() => {
                repositionOnResize();
            }, 100);
        });

        function repositionOnResize() {
            if (isDragging) return;

            const winWidth = window.innerWidth;
            const winHeight = window.innerHeight;
            const rect = fab.getBoundingClientRect();

            let newLeft;
            // Sticky logic
            if (dockedSide === "left") {
                newLeft = isMinimized ? 0 : 20;
            } else {
                newLeft =
                    winWidth - (isMinimized ? 12 : 50) - (isMinimized ? 0 : 20);
            }

            let newTop = rect.top;
            if (newTop < 20) newTop = 20;
            if (newTop > winHeight - 50 - 20) newTop = winHeight - 50 - 20;

            fab.style.left = `${newLeft}px`;
            fab.style.top = `${newTop}px`;

            if (isOpen) {
                adjustMenuPosition(newLeft, newTop);
            }
        }

        function dragStart(e) {
            if (e.type === "touchstart") e.preventDefault();

            fab.classList.remove("snapping");

            isDragging = true;
            hasMoved = false;

            const clientX = e.type.includes("touch")
                ? e.touches[0].clientX
                : e.clientX;
            const clientY = e.type.includes("touch")
                ? e.touches[0].clientY
                : e.clientY;

            startX = clientX;
            startY = clientY;

            const rect = fab.getBoundingClientRect();
            initialFabLeft = rect.left;
            initialFabTop = rect.top;
        }

        function drag(e) {
            if (!isDragging) return;

            // Use requestAnimationFrame for smoother performance
            requestAnimationFrame(() => {
                const clientX = e.type.includes("touch")
                    ? e.touches[0].clientX
                    : e.clientX;
                const clientY = e.type.includes("touch")
                    ? e.touches[0].clientY
                    : e.clientY;

                const dx = clientX - startX;
                const dy = clientY - startY;

                if (
                    Math.abs(dx) > dragThreshold ||
                    Math.abs(dy) > dragThreshold
                ) {
                    hasMoved = true;
                }

                let newLeft = initialFabLeft + dx;
                let newTop = initialFabTop + dy;

                fab.style.left = `${newLeft}px`;
                fab.style.top = `${newTop}px`;
            });
        }

        function dragEnd(e) {
            if (!isDragging) return;
            isDragging = false;

            fab.classList.add("snapping");

            if (hasMoved) {
                snapToEdge(isMinimized);
            } else {
                if (isMinimized) {
                    toggleMinimize(); // Restore on click
                } else {
                    toggleMenu();
                }
            }
            saveState(); // Save position after drag/snap
        }

        function snapToEdge(isMin = false) {
            const rect = fab.getBoundingClientRect();
            const winWidth = window.innerWidth;
            const winHeight = window.innerHeight;

            const distToLeft = rect.left;
            const distToRight = winWidth - rect.right;

            let finalLeft;
            if (distToLeft < distToRight) {
                finalLeft = isMin ? 0 : 20;
                dockedSide = "left";
            } else {
                finalLeft = winWidth - (isMin ? 12 : 50) - (isMin ? 0 : 20);
                dockedSide = "right";
            }

            let finalTop = rect.top;
            if (finalTop < 20) finalTop = 20;
            if (finalTop > winHeight - 50 - 20) finalTop = winHeight - 50 - 20;

            fab.style.left = `${finalLeft}px`;
            fab.style.top = `${finalTop}px`;

            if (isOpen) {
                adjustMenuPosition(finalLeft, finalTop);
            }
        }

        // --- Menu Logic ---

        function toggleMenu(focusSearch = true) {
            if (isMinimized) return; // Should not happen but safety check

            isOpen = !isOpen;
            menu.className = isOpen ? "menu open" : "menu";
            backdrop.style.display = isOpen ? "block" : "none";

            if (isOpen) {
                renderList(routes); // Ensure list is up to date first (for height calc)

                // Use style properties for target position to avoid animation artifacts
                const fabLeft = parseFloat(fab.style.left);
                const fabTop = parseFloat(fab.style.top);
                adjustMenuPosition(fabLeft, fabTop);

                if (focusSearch) {
                    searchInput.focus();
                } else {
                    // Focus on active item or first item
                    setTimeout(() => {
                        focusActiveItem();
                    }, 50); // Small delay to ensure rendering
                }
            }
        }

        function focusActiveItem() {
            const activeItem = ul.querySelector(".route-item.active");
            if (activeItem) {
                activeItem.focus();
            } else {
                const firstItem = ul.querySelector(".route-item");
                if (firstItem) firstItem.focus();
            }
        }

        function adjustMenuPosition(fabLeft, fabTop) {
            const menuWidth = 300;
            const winHeight = window.innerHeight;
            const winWidth = window.innerWidth;

            // Use actual height (with fallback)
            const menuHeight = menu.offsetHeight || 100;

            let menuLeft = fabLeft - menuWidth - 10;
            let menuTop = fabTop + 50 - menuHeight;

            if (fabLeft < winWidth / 2) {
                menuLeft = fabLeft + 60;
            }

            if (menuTop + menuHeight > winHeight - 20) {
                menuTop = winHeight - menuHeight - 20;
            }
            if (menuTop < 20) {
                menuTop = 20;
            }

            menu.style.left = `${menuLeft}px`;
            menu.style.top = `${menuTop}px`;
        }

        searchInput.addEventListener("input", (e) => {
            const term = e.target.value.toLowerCase();
            const filtered = routes.filter(
                (r) =>
                    r.path.toLowerCase().includes(term) ||
                    (r.title && String(r.title).toLowerCase().includes(term))
            );
            renderList(filtered);
        });

        document.addEventListener("keydown", (e) => {
            if (e.key === "Escape" && isOpen) {
                toggleMenu();
            }
            // Cmd+K or Ctrl+K to toggle
            if ((e.metaKey || e.ctrlKey) && e.key === "k") {
                e.preventDefault();

                // If minimized, expand first, then open menu
                if (isMinimized) {
                    toggleMinimize();
                    setTimeout(() => toggleMenu(false), 50);
                    return;
                }

                // If open, close. If closed, open and focus LIST (false).
                if (isOpen) {
                    toggleMenu();
                } else {
                    toggleMenu(false);
                }
            }
        });

        // List Navigation
        ul.addEventListener("keydown", (e) => {
            // Use shadow.activeElement because we are in Shadow DOM
            const active = shadow.activeElement;
            if (!active || !active.classList.contains("route-item")) return;

            if (e.key === "ArrowDown") {
                e.preventDefault();
                const next = active.nextElementSibling;
                if (next) next.focus();
            } else if (e.key === "ArrowUp") {
                e.preventDefault();
                const prev = active.previousElementSibling;
                if (prev) prev.focus();
            } else if (e.key === "Enter") {
                e.preventDefault();
                active.click();
            }
        });

        function renderList(items) {
            ul.innerHTML = "";
            if (items.length === 0) {
                const li = document.createElement("li");
                li.className = "route-item";
                li.style.color = "#999";
                li.style.cursor = "default";
                li.textContent = TEXT.noRoutes;
                ul.appendChild(li);
                return;
            }

            const currentUrl = router ? router.url : "";

            items.forEach((item) => {
                const li = document.createElement("li");
                li.className = "route-item";
                li.tabIndex = 0; // Make focusable

                // Highlight active route (parameter aware matching)
                if (isRouteMatch(item.path, currentUrl)) {
                    li.classList.add("active");
                }

                let content = `<div class="route-path">/${item.path}</div>`;
                if (item.title) {
                    content += `<div class="route-title">${item.title}</div>`;
                }

                const copyBtn = document.createElement("button");
                copyBtn.className = "copy-btn";
                copyBtn.title = "Copy Path";
                copyBtn.innerHTML =
                    '<span class="material-symbols-outlined" style="font-size: 18px;">content_copy</span>';
                copyBtn.onclick = (e) => {
                    e.stopPropagation();
                    const textToCopy = "/" + item.path;
                    navigator.clipboard.writeText(textToCopy).then(() => {
                        // Quick toast or feedback
                        const originalIcon = copyBtn.innerHTML;
                        copyBtn.innerHTML =
                            '<span class="material-symbols-outlined" style="font-size: 18px; color: green;">check</span>';
                        setTimeout(() => {
                            copyBtn.innerHTML = originalIcon;
                        }, 1000);
                    });
                };

                li.innerHTML = content;
                li.appendChild(copyBtn);

                li.onclick = () => {
                    const routePath = item.path;
                    if (routePath.includes(":")) {
                        const newPath = prompt(TEXT.paramPrompt, routePath);
                        if (newPath !== null) {
                            router.navigateByUrl(newPath);
                            // Re-render to update highlight and focus active item
                            setTimeout(() => {
                                renderList(items);
                                focusActiveItem();
                            }, 100);
                        }
                    } else {
                        router.navigateByUrl(routePath);
                        setTimeout(() => {
                            renderList(items);
                            focusActiveItem();
                        }, 100);
                    }
                };
                ul.appendChild(li);
            });
        }

        renderList(routes);

        function refreshRoutes() {
            if (router) {
                routes = extractRoutes(router.config);
                renderList(routes);
            }
        }
    }

    const poller = setInterval(() => {
        pollAttempts++;
        if (initAngularContext()) {
            clearInterval(poller);
        } else if (pollAttempts >= CONFIG.maxPollAttempts) {
            clearInterval(poller);
        }
    }, CONFIG.pollInterval);
})();