Compare commits

..

2 Commits

Author SHA1 Message Date
kang
0e9ef529cc feat(ui): 左侧固定边栏 TOC + 滚动联动高亮
- 两栏布局: 260px sticky sidebar + max 820px 主栏
- TOC 跟随滚动自动高亮当前章节(scrollspy 监听 scroll 事件)
- 平滑跳转 + URL hash 同步, 不依赖 :target 防止 scroll-margin 兼容问题
- 移动端 (< 920px) sidebar 自动折叠成顶部横向标签栏
- 边栏底部加项目元数据小卡片(日期/版本/对比/结论)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:46:23 +08:00
880d4fb2ac auto-save 2026-04-13 18:31 (+1) 2026-04-13 18:31:04 +08:00
2 changed files with 204 additions and 18 deletions

1
graphify Submodule

Submodule graphify added at 04e2960135

View File

@@ -21,8 +21,128 @@
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); -webkit-font-smoothing: antialiased; }
body { line-height: 1.65; font-size: 15px; }
.wrap { max-width: 980px; margin: 0 auto; padding: 48px 28px 96px; }
body { line-height: 1.65; font-size: 15px; scroll-behavior: smooth; }
/* ── two-column layout ───────────────────────────────────────── */
.layout {
max-width: 1280px;
margin: 0 auto;
padding: 0 28px;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 56px;
align-items: start;
}
.main { min-width: 0; padding: 48px 0 96px; max-width: 820px; }
/* ── sticky sidebar + scrollspy TOC ──────────────────────────── */
.sidebar {
position: sticky;
top: 0;
height: 100vh;
padding: 36px 0 28px;
overflow-y: auto;
border-right: 1px solid var(--border);
margin-right: -28px;
padding-right: 28px;
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.sidebar::-webkit-scrollbar-thumb:hover { background: #2f3548; }
.brand {
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
padding-bottom: 10px;
margin-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.brand b { color: var(--accent); font-weight: 600; }
.toc-label {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin: 18px 0 10px;
}
nav.toc { display: flex; flex-direction: column; gap: 2px; }
nav.toc a {
display: block;
padding: 8px 12px 8px 16px;
font-size: 13px;
color: var(--muted);
border-left: 2px solid transparent;
border-radius: 0 6px 6px 0;
border-bottom: none;
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, padding-left 0.15s ease;
line-height: 1.4;
}
nav.toc a:hover {
color: var(--fg);
background: var(--bg-2);
border-left-color: var(--border);
}
nav.toc a.active {
color: var(--accent);
background: linear-gradient(90deg, rgba(122,162,247,0.12), transparent);
border-left-color: var(--accent);
padding-left: 20px;
font-weight: 600;
}
.toc-num { display: inline-block; width: 22px; color: var(--muted); font-family: var(--mono); font-size: 11px; }
nav.toc a.active .toc-num { color: var(--accent); }
.sidebar-foot {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--muted);
line-height: 1.6;
}
.sidebar-foot a { color: var(--accent); border: none; }
.sidebar-foot .row { display: flex; justify-content: space-between; padding: 3px 0; }
.sidebar-foot .k { color: var(--muted); }
/* anchor offset so headings don't get hidden under sticky bars */
h2[id] { scroll-margin-top: 24px; }
/* mobile: sidebar collapses to top bar */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; gap: 0; padding: 0 20px; }
.main { padding-top: 24px; max-width: 100%; }
.sidebar {
position: static;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border);
margin-right: 0;
padding: 18px 0;
}
nav.toc {
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
}
nav.toc a {
border-left: none;
border-bottom: 2px solid transparent;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
}
nav.toc a.active {
padding-left: 10px;
border-left: none;
border-bottom-color: var(--accent);
background: var(--bg-2);
}
.sidebar-foot { display: none; }
}
header { border-bottom: 1px solid var(--border); padding-bottom: 20px; margin-bottom: 40px; }
header .eyebrow { color: var(--muted); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; }
@@ -86,7 +206,31 @@
</style>
</head>
<body>
<div class="wrap">
<div class="layout">
<aside class="sidebar">
<div class="brand">graphify <b>analysis</b></div>
<div class="toc-label">章节</div>
<nav class="toc" id="toc">
<a href="#overview"><span class="toc-num">01</span>架构速览</a>
<a href="#pipeline"><span class="toc-num">02</span>执行流程</a>
<a href="#modules"><span class="toc-num">03</span>逐模块解剖</a>
<a href="#graphrag"><span class="toc-num">04</span>GraphRAG 验证</a>
<a href="#bench"><span class="toc-num">05</span>71.5× 真相</a>
<a href="#highlights"><span class="toc-num">06</span>设计亮点</a>
<a href="#weaknesses"><span class="toc-num">07</span>技术短板</a>
<a href="#vs"><span class="toc-num">08</span>vs GitNexus</a>
<a href="#recommendation"><span class="toc-num">09</span>最终建议</a>
</nav>
<div class="sidebar-foot">
<div class="row"><span class="k">日期</span><span>2026-04-13</span></div>
<div class="row"><span class="k">Graphify</span><span>v0.4.8</span></div>
<div class="row"><span class="k">行数对比</span><span>8.2k vs 31k</span></div>
<div class="row"><span class="k">结论</span><span>不替换</span></div>
</div>
</aside>
<main class="main">
<header>
<div class="eyebrow">源码评估报告</div>
@@ -108,21 +252,6 @@
</div>
</div>
<div class="toc">
<div class="label">目录</div>
<ol>
<li><a href="#overview">一、Graphify 架构速览</a></li>
<li><a href="#pipeline">二、执行流程 &amp; 关键发现</a></li>
<li><a href="#modules">三、核心模块逐个解剖(带行号)</a></li>
<li><a href="#graphrag">四、"GraphRAG" 噱头验证</a></li>
<li><a href="#bench">五、"71.5× token 节省" 真相</a></li>
<li><a href="#highlights">六、设计亮点(可抄)</a></li>
<li><a href="#weaknesses">七、技术短板</a></li>
<li><a href="#vs">八、GitNexus vs Graphify 对比</a></li>
<li><a href="#recommendation">九、最终建议</a></li>
</ol>
</div>
<h2 id="overview">一、Graphify 架构速览</h2>
<p>一句话:<b>tree-sitter AST 提取器 + networkx 图 + Leiden 社区检测 + 10 种导出格式 + 9 种 Agent 平台集成脚本</b>
@@ -539,6 +668,62 @@ resolvers/ workers/</pre>
</p>
</footer>
</main>
</div>
<script>
(function () {
const links = Array.from(document.querySelectorAll('nav.toc a[href^="#"]'));
const map = new Map();
links.forEach(a => {
const id = a.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (target) map.set(target, a);
});
let activeLink = null;
const setActive = (link) => {
if (activeLink === link) return;
if (activeLink) activeLink.classList.remove('active');
if (link) {
link.classList.add('active');
// keep active item in view inside the sidebar scroll area
link.scrollIntoView({ block: 'nearest' });
}
activeLink = link;
};
// Scroll spy: pick the section whose top is closest above viewport's 30% line.
// IntersectionObserver alone is jumpy with long sections, so we listen to scroll
// and recompute manually.
const sections = Array.from(map.keys());
const onScroll = () => {
const probe = window.innerHeight * 0.3;
let current = sections[0];
for (const s of sections) {
const top = s.getBoundingClientRect().top;
if (top - probe <= 0) current = s;
else break;
}
setActive(map.get(current));
};
// Smooth scroll override (avoids :target jump that ignores scroll-margin on some browsers)
links.forEach(a => {
a.addEventListener('click', (e) => {
const id = a.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
e.preventDefault();
window.scrollTo({ top: target.getBoundingClientRect().top + window.scrollY - 18, behavior: 'smooth' });
history.replaceState(null, '', '#' + id);
});
});
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
onScroll();
})();
</script>
</body>
</html>