Skip to content

feat: 基于原生目录折叠深层分支并保留前两层 - #152

Open
Yuna-Celisse wants to merge 3 commits into
mainfrom
codex/adaptive-page-outline
Open

feat: 基于原生目录折叠深层分支并保留前两层#152
Yuna-Celisse wants to merge 3 commits into
mainfrom
codex/adaptive-page-outline

Conversation

@Yuna-Celisse

@Yuna-Celisse Yuna-Celisse commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

长文目录需要展示更深的标题,同时保留前两层供快速定位。本次直接使用 VitePress 的原生目录树和 active-anchor 引擎,仅在 CSS 渲染层折叠深层分支:桌面前两层始终显示,第三层及以下随当前活动分支或键盘焦点展开。移动端固定显示原生树的前两层。

默认标题范围为 h2–h4;跳级标题沿用原生 buildTree 的提升规则。没有自定义 DOM 扫描器、额外的 scroll/resize 监听器,也没有隐藏另一个仍在运行的桌面目录。

原生 useActiveAnchor 负责 isBottom 激活末尾标题、100ms throttleAndDebounce、isAsideEnabled 可见性判断和独立的 outline-marker。深层链接保留在 DOM 中,原生引擎添加 active 类后 CSS 立即展开对应分支,然后原生指示条测量位置。没有随滚动执行的高度动画;不支持 :has 的浏览器回退到完整原生桌面目录。

Scope

  • Tutorial
  • Process
  • Repair
  • Archived
  • Navigation
  • Assets or templates
  • CI or tooling
  • Governance docs

Checklist

  • I updated the relevant sidebar or confirmed no navigation change is needed.
  • I checked internal links and asset paths affected by this PR.
  • I kept historical archived content unchanged unless this PR is explicitly about archive maintenance.
  • I documented follow-up work that is intentionally outside this PR.

Verification

本地使用已安装依赖的 CLI(Node 24、独立 LF 检出);标准 Node 22 / pnpm 9 环境由 Linux CI 验证。

  • Linux CI(Node 22 / pnpm 9):测试、lint、构建、Verify rendered output 全部通过。运行记录
  • Vitest:9 个文件、67 项测试通过。
  • ESLint 全仓、Markdownlint 222 个文件及 git diff --check 通过。
  • VitePress 生产构建通过。
  • 浏览器:硬件手册前两层常驻,进入「如何评估硬件参数」显示四级目录,离开后折叠;点击 CPU 子标题后指示条正确对齐。
  • 浏览器:2009 年维修队简介滚动到底部后,最后的四级标题「直接打电话给干事(特殊)」激活且可见,指示条正确对齐。
  • 浏览器:390px 手机宽度下桌面侧栏隐藏,移动菜单保留前两层,深层子标题不拉长菜单。

新增 7 项原生引擎回归测试,直接调用安装的 VitePress 代码,覆盖跳级标题、忽略子树、页面级范围/禁用、标题序列化、底部激活与指示条、隐藏侧栏不测量、100ms 节流及尾随调用、监听器卸载。Vitest 配置仅增加对 VitePress 浏览器模块的转换,以解析其无扩展名的导入。

Notes

保留 wen-templari 提出的前两层常驻,并按 m1ngsama 建议复用原生数据、只修改渲染层。生产侧新增的逻辑仅为样式,不增加自定义 Vue 组件或 JS 滚动处理。

源码核实:VitePress 1.6.4 的原生 getHeaders 在 onContentUpdated 中读取标题,并非构建时生成完整目录;本 PR 保持原生 SSR/客户端行为,不另行承诺静态目录。后续升级 VitePress 时需确认目录 DOM 类名和原生引擎契约,现有回归测试用于检测行为变化。

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 5, 2026

Copy link
Copy Markdown

Deploying documents with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5bf0d69
Status: ✅  Deploy successful!
Preview URL: https://492194d5.documents-dq4.pages.dev
Branch Preview URL: https://codex-adaptive-page-outline.documents-dq4.pages.dev

View logs

@m1ngsama m1ngsama left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful write-up and the verification notes — the underlying need is real, long tutorial pages do want a deeper outline. But I can't merge this as it stands: two of the defects below mean the feature does not actually work as described, and the approach itself costs more than it returns for this site.

Scope and positioning

  • .vitepress/theme/ is currently 17 files / 794 lines; this PR adds 276 lines in one file, growing the theme layer by ~35% for a single interaction nicety.
  • Roughly 90% of the visible gain comes from the one-line outline.level: [2, 4] in config.mts; the component only adds "collapse the non-active sections".
  • Layout.vue hides .VPDocAsideOutline with display: none instead of replacing it, so the native component stays mounted and its useActiveAnchor scroll handler keeps running alongside the new one — two outline engines on every page.
  • The native outline is built from build-time page data and ships in the static HTML; CurrentOutline scans the DOM in onMounted, so the desktop outline is empty in the build output. That is a real regression for a static docs site.

Cross-device behaviour

  • outline.level: [2, 4] is global and also feeds the mobile VPLocalNav dropdown (getHeaders(frontmatter.outline ?? theme.outline)). The default is 2, so mobile currently lists only h2; after this change it flattens h2/h3/h4 into one dropdown — 53 entries on tutorial/manual/windows-from-scratch.md — with none of the collapsing this PR adds. The PR notes mobile keeps the native outline, but not that the native outline gets several times longer.
  • The aside is display: none below 1280px, yet the component still mounts there. On phones, tablets and narrow desktop windows the scroll handler runs one getBoundingClientRect() per heading per animation frame — 53 forced layout reads per frame on that same page — for UI nobody can see. VitePress guards this with if (!isAsideEnabled.value) return plus throttleAndDebounce(fn, 100); neither was ported.
  • Desktop and mobile will list different entries: desktop uses the custom grouping (which drops level-skipping headings), mobile uses the native buildTree (which keeps them).

Defects, most severe first

  1. CurrentOutline.vue:228 — the active marker never renders. .current-outline-link is both position: relative and overflow: hidden (needed for the ellipsis), so its own ::before at left: -17px is clipped by its containing block. Even without overflow, -17px only lines up for level two; the nested <ol>s add 14px and 14+12px of padding, putting the bar inside the text column for levels three and four. VitePress sidesteps this by making .outline-marker a sibling in .content, not a child of the link.
  2. CurrentOutline.vue:70 — nothing in the last viewport ever activates. The test is absoluteTop <= scrollY + getScrollOffset() + 4, and at maximum scroll scrollY = docHeight - innerHeight, so any heading in the final innerHeight - scrollOffset pixels (~700px on a laptop) can never match. Since children only render when activeSectionId === section.id, the last ## of every page never expands its subheadings — exactly the case this PR is meant to serve. VitePress's setActiveLink has an explicit isBottom branch for this; the port dropped it.
  3. CurrentOutline.vue:55-61 — level-skipping headings are silently dropped: h4 is kept only after an h3, h3 only after an h2. archived/2023/developer/2023-10-newcomer-training.md:20,26 hits this today — #### 基础 and #### 进阶 sit under a list-nested ## Blog From Scratch with no ### between, so both vanish from the desktop outline with no fallback, since the native one is hidden unconditionally.
  4. CurrentOutline.vue:41 — the ignore-header guard mis-parents. On an ignored h2 the loop continues without resetting currentSection/currentSubsection, so every following h3 is pushed into the previous h2 and appears when the wrong section is active. No page uses {.ignore-header} yet, so it is latent, but the guard as written is worse than none. VitePress marks the whole ignored subtree instead.
  5. CurrentOutline.vue:127-128 — no isAsideEnabled guard and no throttle on the scroll/resize listeners (see cross-device note above).
  6. CurrentOutline.vue:29-33headingTitle strips only .header-anchor; VitePress's serializeHeader also drops VPBadge, footnote-ref and ignore-header children. No heading contains a badge today, so this is a robustness gap rather than a live bug.

Smaller points

  • 276 new lines with no test. The repo has 8 vitest files; the readHeadings grouping is plain, testable logic.
  • Active items are missing aria-current, which the native outline sets.
  • Only frontmatter.outline === false is honoured; a per-page level override is ignored.
  • The checklist marks browser verification of the two→three→four expansion, but items 1 and 2 are visible at a glance in a browser, so that pass needs redoing.
  • The four verify-dist failures were attributed to backslash paths on Windows and left unconfirmed; please settle that on Linux CI before the next round.

Suggested path

My preference is the minimal one: keep outline.level: [2, 4] in config.mts and drop the component. One line gets the deeper outline, with zero maintenance, identical desktop and mobile output, and the static HTML intact. The cost is no collapsing, which I think is the right trade for a site optimised for being correct and durable rather than clever. If the longer mobile dropdown is a concern we can settle on [2, 3] separately.

If you want to keep the collapsing behaviour, please rebuild it on the native data rather than a DOM scan: reuse getHeaders/buildTree and add collapsing only in the render layer, then add the isAsideEnabled guard, the 100ms throttle and the isBottom branch, move the marker to a sibling of the links, and cover the grouping with a unit test. That keeps the two devices consistent and the build-time outline intact.

Either way the direction is worth having — happy to review the follow-up.

@wen-templari

Copy link
Copy Markdown
Member

试用下来感觉在快速滑动或是有较多子标题/层级时,UI变化过大。我们目前只显示了一级标题,是否可以改成在目录中持续显示一级和二级标题。

快速滑动时UI闪烁
录屏2026-09-12 13 47 40

Vite,在目录中显示一级和二级标题
截屏2026-09-12 13 53 32

@Yuna-Celisse Yuna-Celisse changed the title feat: 移植 homepage 的多级目录自动展开与隐藏 feat: 本页目录持续显示前两层标题 Sep 12, 2026
@Yuna-Celisse Yuna-Celisse changed the title feat: 本页目录持续显示前两层标题 feat: 基于原生目录折叠深层分支并保留前两层 Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants