FolioTreeNav 是 Folio 唯一的左侧文档导航组件。它接收 Core 生成的导航树,渲染文档分类、内容父页和子页面;右侧 FolioToc 负责文章内部标题,两者职责不同。
使用方式#
import {
buildFolioNavigationTree,
FolioTreeNav,
} from "@uichat-mira/folio";
// 当前文章来自构建层解析后的 docs。
const tree = buildFolioNavigationTree(docs, currentDoc);
<FolioTreeNav tree={tree} currentPath={currentDoc.path} />;独立使用时需位于 React Router 内,支持 basename、className、ariaLabel 和 onNavigate。在 FolioApp 中无需手动构造 tree:默认阅读布局已接线;如需替换整套侧栏,查看 FolioApp 的导航插槽示例。
内容导航与展开行为#
collection / section是不可点击、不可折叠的分类标签,不生成虚假的页面链接。- 有真实概览页的内容父节点:标题是路由链接,右侧
+ / −是独立展开按钮,两者共用整行悬停背景。 - 没有真实概览的隐式目录:仅显示目录标题,不创建会产生 404 的链接。
- 当前页高亮强于悬停高亮;当前页的祖先自动展开。
FolioApp将展开状态保存在阅读路由之外,切换文章时仍然保留用户手动展开的子树。- 展开、收起有内容高度动画;折叠的子链接不可聚焦,并尊重系统的减少动态效果设置。
FolioTreeExpansionProps支持defaultExpanded、expandedPaths、onExpandChange。使用collapsible: false时不显示按钮,但全部子页面仍然可见、可聚焦。
统一模型:buildFolioNavigationTree#
import {
buildFolioNavigationTree,
type FolioNavigationTree,
} from "@uichat-mira/folio";
const tree: FolioNavigationTree = buildFolioNavigationTree(docs, currentDoc);
// 明确要组合跨路由区域的内容时,才声明导航范围:
const shared = buildFolioNavigationTree(docs, currentDoc, {
scopePath: "/reference",
});它是 React-free 的 Core 函数,负责把 type: doc 的 collection / section 分组与 type: project 的目录层级组织成同一棵树。默认项目归属依据各集合实际包含文档的目录,不会因为多个目录的公共祖先恰好为 / 而把全站项目混进来。同名分类也不会跨集合合并;无法唯一归属的项目保留独立分组。
scopePath 允许消费者明确扩大路由范围,但不等于创建权限系统或后台可编辑菜单。运行时导航与静态输出使用同一份 Core 树。
纯目录模型:buildFolioContentTree#
只需要某一个项目目录时,可以独立构建纯内容树:
import {
buildFolioContentTree,
resolveFolioProjectTree,
getFolioTreeNeighbors,
type FolioContentTree,
type FolioContentTreeNode,
} from "@uichat-mira/folio";
const projectTree = buildFolioContentTree(
docs.filter((doc) => doc.type === "project"),
{ rootPath: "/projects" },
);
const { previous, next } = getFolioTreeNeighbors(
projectTree,
"/projects/atlas/notes",
);主要数据契约如下:
type FolioContentTree = {
rootPath: string;
roots: FolioContentTreeNode[];
scopeId?: string; // 可选的导航作用域身份
};
type FolioContentTreeNode = {
path: string;
title: string;
kind?: "section" | "page" | "directory";
doc?: FolioDoc; // 没有概览页时为 undefined
children: FolioContentTreeNode[];
};rootPath 是树的容器路径:如果把它设置成概览页本身的地址,返回的是其后代,不包含该概览页。resolveFolioProjectTree(docs, currentDoc) 则根据当前项目路由推导默认范围。
父级推导与缺失概览#
Folio 从解析后的最终路由路径推导父子关系,不需要手工填写 parent 或 parentId。例如:
projects/
atlas/
index.md # /projects/atlas,父级可点击
notes/
index.md # /projects/atlas/notes,子页面
experiment/
notes/
index.md # /projects/experiment/notessourcePathToRoute() 把 index.md 映射为目录自身的路由。由于 projects/experiment/index.md 不存在,/projects/experiment 仍会作为树上的隐式目录,但没有 doc,因此不会生成虚假链接。
还要留意以下约束:
- Frontmatter 显式覆写
path时,以最终路由为准,而不是原始文件夹位置。 - 路由按路径段匹配,
/projects/com不会错误包含/projects/com-design。 - 重复的最终路由在构建树时抛错,不会静默覆盖文档。
排序与上一篇/下一篇#
- 有真实
doc的节点按 Frontmatterorder排序;缺省由parseFolioDoc设为99。 - 隐式目录没有自身的
order,使用子节点最小的排序值,不会再把值大于 99 的子节点强行截断。 - 排序值相同,按路由路径字典序确定稳定顺序。
getFolioNavigationNeighbors()是默认阅读分页的顺序来源:按左侧统一树先序遍历有效页面,跨越同一文档导航中的分类及项目子文章。getFolioTreeNeighbors()是保留的纯项目工具函数,仅在当前一级项目树内先序遍历,不跨到相邻项目。没有文档分类的项目默认仍遵循这条边界。
这些规则适用于任意深度的真实内容树,但不意味着文档中心本身必须保留五层页面。
静态输出边界#
无 JavaScript 的静态 HTML 使用同一套树数据,保留所有有效子页面链接和层级;它不会渲染无法操作的 + / − 控件,也不会把 React 的手动展开状态持久化到静态文件。
FolioTreeNav 负责展示和导航,不处理权限、内容审核或发布审批。相关的全站应用插槽见 FolioApp。