| … | … | @@ -508,6 +508,7 @@ h2 { font-size: 14px; color: #8b949e; margin-bottom: 6px; font-weight: normal; } |
| 508 | 508 | <button class="tab main-tab active" data-tab="tree">Tree</button> |
| 509 | 509 | <button class="tab main-tab" data-tab="graph">Graph</button> |
| 510 | 510 | <button class="tab main-tab" data-tab="validate">Validate</button> |
| 511 | <button class="tab main-tab" data-tab="docs">Docs</button> |
| 511 | 512 | </div> |
| 512 | 513 | <input class="search" id="search" type="text" placeholder="Filter modules, files, symbols..."> |
| 513 | 514 | <div style="display:flex;gap:4px;margin-bottom:8px"> |
| … | … | @@ -606,6 +607,13 @@ document.querySelectorAll('.main-tab').forEach(tab => { |
| 606 | 607 | // Don't change center content — revert to previous tab's center |
| 607 | 608 | detail.style.display = '' |
| 608 | 609 | graphContainer.classList.remove('active') |
| 610 | } else if (currentTab === 'docs') { |
| 611 | // Docs tab = show doc cruft report in the center pane |
| 612 | rightPanel.style.display = 'none' |
| 613 | resizeRight.style.display = 'none' |
| 614 | detail.style.display = '' |
| 615 | graphContainer.classList.remove('active') |
| 616 | renderDocsCruft() |
| 609 | 617 | } else if (currentTab === 'graph') { |
| 610 | 618 | if (!graphInitialized) { initGraph(); updateBreadcrumb() } |
| 611 | 619 | } |
| … | … | @@ -1927,6 +1935,14 @@ function urlFromGraphStack (stack) { |
| 1927 | 1935 | } |
| 1928 | 1936 | |
| 1929 | 1937 | function graphStackFromUrl (pathname) { |
| 1938 | return routeFromUrl(pathname).stack |
| 1939 | } |
| 1940 | |
| 1941 | // Parse URL into a full route object. |
| 1942 | // kind: 'system' | 'module-graph' | 'path-graph' | 'validation' | 'docs' | 'suggestions' |
| 1943 | // stack: graph stack (only meaningful for *-graph kinds) |
| 1944 | // extra: { moduleName (validation), docPath (docs), suggestPath (suggestions) } |
| 1945 | function routeFromUrl (pathname) { |
| 1930 | 1946 | const base = _basePrefix() |
| 1931 | 1947 | let p = pathname || '/' |
| 1932 | 1948 | if (base && p.startsWith(base)) p = p.slice(base.length) || '/' |
| … | … | @@ -1934,10 +1950,25 @@ function graphStackFromUrl (pathname) { |
| 1934 | 1950 | // Normalize: drop trailing slash (except root) |
| 1935 | 1951 | if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1) |
| 1936 | 1952 | |
| 1937 | | if (p === '/' || p === '/system' || p === '') return [] |
| 1953 | if (p === '/' || p === '/system' || p === '') return { kind: 'system', stack: [], extra: {} } |
| 1938 | 1954 | |
| 1939 | 1955 | const segs = p.split('/').filter(Boolean) |
| 1940 | 1956 | |
| 1957 | if (segs[0] === 'validation') { |
| 1958 | const moduleName = (segs[1] === 'module' && segs[2]) ? decodeURIComponent(segs[2]) : null |
| 1959 | return { kind: 'validation', stack: [], extra: { moduleName } } |
| 1960 | } |
| 1961 | |
| 1962 | if (segs[0] === 'docs') { |
| 1963 | const docPath = segs.length > 1 ? segs.slice(1).map(decodeURIComponent).join('/') : null |
| 1964 | return { kind: 'docs', stack: [], extra: { docPath } } |
| 1965 | } |
| 1966 | |
| 1967 | if (segs[0] === 'suggestions' && segs.length > 1) { |
| 1968 | const suggestPath = segs.slice(1).map(decodeURIComponent).join('/') |
| 1969 | return { kind: 'suggestions', stack: [], extra: { suggestPath } } |
| 1970 | } |
| 1971 | |
| 1941 | 1972 | if (segs[0] === 'module' && segs[1]) { |
| 1942 | 1973 | const moduleName = decodeURIComponent(segs[1]) |
| 1943 | 1974 | const stack = [{ type: 'module', name: moduleName, label: moduleName }] |
| … | … | @@ -1950,19 +1981,18 @@ function graphStackFromUrl (pathname) { |
| 1950 | 1981 | label: filePath.split('/').pop() |
| 1951 | 1982 | }) |
| 1952 | 1983 | } |
| 1953 | | return stack |
| 1984 | return { kind: 'module-graph', stack, extra: {} } |
| 1954 | 1985 | } |
| 1955 | 1986 | |
| 1956 | 1987 | if (segs[0] === 'path' && segs.length > 1) { |
| 1957 | | return segs.slice(1).map(s => { |
| 1988 | const stack = segs.slice(1).map(s => { |
| 1958 | 1989 | const name = decodeURIComponent(s) |
| 1959 | 1990 | return { type: 'path', name, label: name } |
| 1960 | 1991 | }) |
| 1992 | return { kind: 'path-graph', stack, extra: {} } |
| 1961 | 1993 | } |
| 1962 | 1994 | |
| 1963 | | // Unknown paths (e.g. /validation, /docs — Phase 0e) → leave stack empty, |
| 1964 | | // let the respective panels handle their own routing. |
| 1965 | | return [] |
| 1995 | return { kind: 'system', stack: [], extra: {} } |
| 1966 | 1996 | } |
| 1967 | 1997 | |
| 1968 | 1998 | // Set to true inside popstate handling so nav functions skip pushing again. |
| … | … | @@ -2016,6 +2046,91 @@ function applyStackFromUrl (stack) { |
| 2016 | 2046 | } |
| 2017 | 2047 | } |
| 2018 | 2048 | |
| 2049 | // Apply a full route object (the post-0e extension of applyStackFromUrl). |
| 2050 | // Activates the right main tab and renders whatever view the URL describes. |
| 2051 | function applyRouteFromUrl (route) { |
| 2052 | _suppressPushState = true |
| 2053 | try { |
| 2054 | switch (route.kind) { |
| 2055 | case 'validation': { |
| 2056 | activateMainTab('validate') |
| 2057 | if (!valInitialized) initValidation() |
| 2058 | switchRightPanel('validation') |
| 2059 | if (route.extra.moduleName) { |
| 2060 | renderValidation(route.extra.moduleName) |
| 2061 | } else { |
| 2062 | renderValidation(null) |
| 2063 | } |
| 2064 | break |
| 2065 | } |
| 2066 | case 'docs': { |
| 2067 | activateMainTab('docs') |
| 2068 | renderDocsCruft(route.extra.docPath) |
| 2069 | break |
| 2070 | } |
| 2071 | case 'suggestions': { |
| 2072 | // Phase 0e leaves suggestions as a SSR-style placeholder — codetree |
| 2073 | // serve can't currently run --suggest per file on demand. Show a |
| 2074 | // friendly placeholder that points users at the CLI. |
| 2075 | activateMainTab('docs') |
| 2076 | const detail = document.getElementById('detail') |
| 2077 | if (detail) { |
| 2078 | detail.innerHTML = '<div style="padding:20px;color:#8b949e;max-width:800px;margin:0 auto">' + |
| 2079 | '<h1 style="color:#58a6ff;font-size:1.3rem">Suggested fixes for ' + |
| 2080 | escapeHtml(route.extra.suggestPath || '') + '</h1>' + |
| 2081 | '<p>Run <code style="background:#161b22;padding:2px 6px;border-radius:3px">' + |
| 2082 | 'codetree docs . --suggest --target ' + escapeHtml(route.extra.suggestPath || '') + '</code> ' + |
| 2083 | 'to generate a patch preview on the command line. Live per-file suggestions via /suggestions/* ' + |
| 2084 | 'will land in a later phase.</p>' + |
| 2085 | '</div>' |
| 2086 | } |
| 2087 | break |
| 2088 | } |
| 2089 | case 'module-graph': |
| 2090 | case 'path-graph': |
| 2091 | case 'system': |
| 2092 | default: |
| 2093 | applyStackFromUrl(route.stack) |
| 2094 | break |
| 2095 | } |
| 2096 | } finally { |
| 2097 | _suppressPushState = false |
| 2098 | } |
| 2099 | } |
| 2100 | |
| 2101 | function activateMainTab (name) { |
| 2102 | const tabs = document.querySelectorAll('.main-tab') |
| 2103 | for (const t of tabs) { |
| 2104 | if (t.dataset.tab === name && !t.classList.contains('active')) { |
| 2105 | t.click() |
| 2106 | return |
| 2107 | } |
| 2108 | } |
| 2109 | } |
| 2110 | |
| 2111 | // Programmatic nav helpers — used by links inside rendered panels. |
| 2112 | window.navigateToValidation = function (moduleName) { |
| 2113 | const base = _basePrefix() |
| 2114 | const url = moduleName |
| 2115 | ? base + '/validation/module/' + encodeURIComponent(moduleName) |
| 2116 | : base + '/validation' |
| 2117 | try { |
| 2118 | window.history.pushState({}, '', url) |
| 2119 | } catch {} |
| 2120 | applyRouteFromUrl(routeFromUrl(url)) |
| 2121 | } |
| 2122 | |
| 2123 | window.navigateToDocs = function (docPath) { |
| 2124 | const base = _basePrefix() |
| 2125 | const url = docPath |
| 2126 | ? base + '/docs/' + docPath.split('/').map(encodeURIComponent).join('/') |
| 2127 | : base + '/docs' |
| 2128 | try { |
| 2129 | window.history.pushState({}, '', url) |
| 2130 | } catch {} |
| 2131 | applyRouteFromUrl(routeFromUrl(url)) |
| 2132 | } |
| 2133 | |
| 2019 | 2134 | function updateBreadcrumb () { |
| 2020 | 2135 | const bc = document.getElementById('graph-breadcrumb') |
| 2021 | 2136 | // Render breadcrumb segments as real <a href> so middle-click / |
| … | … | @@ -3529,6 +3644,95 @@ function valBadgeHTML (name) { |
| 3529 | 3644 | return '<span class="val-badge val-badge-pass">' + v.pass + ' ok</span>' |
| 3530 | 3645 | } |
| 3531 | 3646 | |
| 3647 | // ====== DOCS CRUFT VIEW (Phase 0e) ====== |
| 3648 | // Renders the docCruft report in the main content area when the Docs |
| 3649 | // main-tab is active. Each doc is deep-linkable at /docs/<path>. |
| 3650 | function renderDocsCruft (focusPath) { |
| 3651 | const detail = document.getElementById('detail') |
| 3652 | if (!detail) return |
| 3653 | const cruft = tree.docCruft |
| 3654 | if (!cruft) { |
| 3655 | detail.innerHTML = '<div style="padding:20px;color:#8b949e">No doc cruft report available. Run codetree on a project with markdown files to see this view.</div>' |
| 3656 | return |
| 3657 | } |
| 3658 | |
| 3659 | let html = '<div style="padding:16px;max-width:1000px;margin:0 auto">' |
| 3660 | html += '<h1 style="color:#58a6ff;font-size:1.4rem;margin-bottom:0.5rem">Documentation cruft</h1>' |
| 3661 | html += '<div style="color:#8b949e;font-size:0.85rem;margin-bottom:1.5rem">' |
| 3662 | html += cruft.summary.totalDocs + ' docs, ' + cruft.summary.totalLines + ' lines · ' |
| 3663 | html += '<span style="color:#3fb950">' + cruft.summary.freshDocs + ' fresh</span> · ' |
| 3664 | html += '<span style="color:#d29922">' + cruft.summary.staleDocs + ' stale</span> · ' |
| 3665 | html += cruft.summary.validReferences + ' valid, ' |
| 3666 | html += cruft.summary.missingReferences + ' missing refs' |
| 3667 | if (cruft.summary.externalReferences) { |
| 3668 | html += ', ' + cruft.summary.externalReferences + ' external' |
| 3669 | } |
| 3670 | html += '</div>' |
| 3671 | |
| 3672 | for (const doc of cruft.docs) { |
| 3673 | if (doc.references.total === 0 && doc.staleness.score < 0.3) continue |
| 3674 | |
| 3675 | const score = doc.staleness.score |
| 3676 | const icon = score > 0.5 ? 'STALE' : score > 0.3 ? 'WARN' : 'FRESH' |
| 3677 | const color = score > 0.5 ? '#f85149' : score > 0.3 ? '#d29922' : '#3fb950' |
| 3678 | |
| 3679 | const docHref = (API || '') + '/docs/' + doc.path.split('/').map(encodeURIComponent).join('/') |
| 3680 | const open = (focusPath && doc.path === focusPath) ? ' open' : '' |
| 3681 | |
| 3682 | html += '<details' + open + ' style="border:1px solid #21262d;border-radius:6px;padding:8px 12px;margin-bottom:6px;background:#0d1117">' |
| 3683 | html += '<summary style="cursor:pointer;display:flex;align-items:center;gap:0.75rem">' |
| 3684 | html += '<span style="font-family:monospace;font-size:0.7rem;color:' + color + ';min-width:44px">' + icon + '</span>' |
| 3685 | html += '<a href="' + docHref + '" style="color:#58a6ff;text-decoration:none;font-family:monospace" onclick="event.stopPropagation()">' + escapeHtml(doc.path) + '</a>' |
| 3686 | html += '<span style="color:#6e7681;font-size:0.75rem;margin-left:auto">' |
| 3687 | html += doc.lines + ' lines · ' + doc.lastModified + ' · score ' + score |
| 3688 | html += '</span>' |
| 3689 | html += '</summary>' |
| 3690 | |
| 3691 | // Per-ref details, grouped by status |
| 3692 | const missing = doc.references.details.filter(d => d.status === 'missing') |
| 3693 | const external = doc.references.details.filter(d => d.status === 'external') |
| 3694 | const valid = doc.references.details.filter(d => d.status === 'valid') |
| 3695 | |
| 3696 | html += '<div style="margin-top:10px;font-size:0.8rem">' |
| 3697 | html += '<div style="color:#8b949e;margin-bottom:6px">' |
| 3698 | html += valid.length + ' valid · ' + missing.length + ' missing' |
| 3699 | if (external.length) html += ' · ' + external.length + ' external' |
| 3700 | html += '</div>' |
| 3701 | |
| 3702 | if (missing.length > 0) { |
| 3703 | html += '<div style="margin-bottom:8px"><span style="color:#f85149;font-weight:bold">Missing (' + missing.length + '):</span><ul style="margin-top:4px;padding-left:20px;color:#c9d1d9">' |
| 3704 | for (const m of missing.slice(0, 30)) { |
| 3705 | html += '<li><code>' + escapeHtml(m.ref) + '</code> <span style="color:#6e7681">(' + m.type + ', line ' + m.line + ')</span></li>' |
| 3706 | } |
| 3707 | if (missing.length > 30) html += '<li style="color:#6e7681">… and ' + (missing.length - 30) + ' more</li>' |
| 3708 | html += '</ul></div>' |
| 3709 | } |
| 3710 | |
| 3711 | if (external.length > 0) { |
| 3712 | html += '<div style="margin-bottom:8px"><span style="color:#6e7681;font-weight:bold">External (' + external.length + '):</span> <span style="color:#6e7681">' + |
| 3713 | external.slice(0, 10).map(e => '<code>' + escapeHtml(e.ref) + '</code>').join(', ') + |
| 3714 | (external.length > 10 ? ', …' : '') + '</span></div>' |
| 3715 | } |
| 3716 | |
| 3717 | html += '</div></details>' |
| 3718 | } |
| 3719 | html += '</div>' |
| 3720 | |
| 3721 | detail.innerHTML = html |
| 3722 | |
| 3723 | // Intercept clicks on doc links for SPA nav |
| 3724 | for (const a of detail.querySelectorAll('a[href^="' + (API || '') + '/docs/"]')) { |
| 3725 | a.addEventListener('click', (e) => { |
| 3726 | if (e.button !== 0) return |
| 3727 | if (e.metaKey || e.ctrlKey || e.shiftKey) return |
| 3728 | e.preventDefault() |
| 3729 | const href = a.getAttribute('href') |
| 3730 | const path = href.replace((API || '') + '/docs/', '').split('/').map(decodeURIComponent).join('/') |
| 3731 | window.navigateToDocs(path) |
| 3732 | }) |
| 3733 | } |
| 3734 | } |
| 3735 | |
| 3532 | 3736 | // ====== TREE DEP HIGHLIGHTING ====== |
| 3533 | 3737 | function highlightTreeDeps (moduleName) { |
| 3534 | 3738 | // Clear previous highlights |
| … | … | @@ -3680,18 +3884,16 @@ buildTree() |
| 3680 | 3884 | initResizable() |
| 3681 | 3885 | |
| 3682 | 3886 | // ====== URL ROUTING — popstate + initial URL ====== |
| 3683 | | // On cold page load, parse location.pathname to derive the starting |
| 3684 | | // graphStack. If it's non-empty (deep link), switch to the graph tab |
| 3685 | | // and apply the stack. Otherwise stay on the default tree view. |
| 3686 | | // |
| 3687 | | // popstate (back/forward button) re-derives the stack from the URL |
| 3688 | | // and applies it without pushing new history. |
| 3887 | // On cold page load, parse location.pathname and route to the right view. |
| 3888 | // popstate (back/forward button) re-derives the route from the URL and |
| 3889 | // applies it without pushing new history. |
| 3689 | 3890 | (function initUrlRouting () { |
| 3690 | | window.addEventListener('popstate', (ev) => { |
| 3691 | | const stack = graphStackFromUrl(window.location.pathname) |
| 3692 | | applyStackFromUrl(stack) |
| 3693 | | // Make sure we're in the graph tab if we're displaying graph content |
| 3694 | | if (stack.length > 0) ensureGraphTabActive() |
| 3891 | window.addEventListener('popstate', () => { |
| 3892 | const route = routeFromUrl(window.location.pathname) |
| 3893 | if (route.kind === 'module-graph' || route.kind === 'path-graph') { |
| 3894 | ensureGraphTabActive() |
| 3895 | } |
| 3896 | applyRouteFromUrl(route) |
| 3695 | 3897 | }) |
| 3696 | 3898 | |
| 3697 | 3899 | function ensureGraphTabActive () { |
| … | … | @@ -3704,19 +3906,19 @@ initResizable() |
| 3704 | 3906 | } |
| 3705 | 3907 | } |
| 3706 | 3908 | |
| 3707 | | const initialStack = graphStackFromUrl(window.location.pathname) |
| 3708 | | if (initialStack.length > 0) { |
| 3709 | | // Deep link — switch to graph tab and render the stack. Also replace |
| 3710 | | // state (not push) so we start with a correct history entry. |
| 3909 | const initialRoute = routeFromUrl(window.location.pathname) |
| 3910 | if (initialRoute.kind === 'module-graph' || initialRoute.kind === 'path-graph') { |
| 3711 | 3911 | ensureGraphTabActive() |
| 3712 | | // Let the graph tab initialize first, then apply the stack. |
| 3713 | | // Using a microtask so initGraph() has run. |
| 3714 | 3912 | Promise.resolve().then(() => { |
| 3715 | | applyStackFromUrl(initialStack) |
| 3913 | applyRouteFromUrl(initialRoute) |
| 3716 | 3914 | replaceStateForStack() |
| 3717 | 3915 | }) |
| 3916 | } else if (initialRoute.kind !== 'system') { |
| 3917 | // /validation, /docs, /suggestions — apply directly, the handlers switch tabs |
| 3918 | Promise.resolve().then(() => { |
| 3919 | applyRouteFromUrl(initialRoute) |
| 3920 | }) |
| 3718 | 3921 | } else { |
| 3719 | | // System overview — seed history with a canonical URL. |
| 3720 | 3922 | replaceStateForStack() |
| 3721 | 3923 | } |
| 3722 | 3924 | })() |