diff --git a/docs/config/markdown.md b/docs/config/markdown.md
index 9af76e0db8..f5641821af 100644
--- a/docs/config/markdown.md
+++ b/docs/config/markdown.md
@@ -84,9 +84,19 @@ title: 配置页面标题
## nav
-- 类型:`string | { title: string; order: number; parent: { title: string; order: string } }`
+- 类型:`string | { title: string; order: number; second: string | { title: string; order: string } }`
- 默认值:`undefined`
+
+
+:::warning
+二级导航为 dumi v2.2 的新增特性,由于二级导航特性会影响主题 API 的行为,为了保证对存量项目及主题包的向前兼容,dumi 仅在项目 `devDependencies` 中声明的 dumi 版本号大于等于 `2.2.0`(例如 `^2.2.0`)时才会启用该特性
+
+如果你的项目使用了三方主题包,能否使用二级导航则取决于主题包是否适配该特性,dumi 会依据主题包 `peerDependencies` 中声明的 dumi 版本作为判断依据
+:::
+
+
+
配置当前页所属的一级导航及二级导航,同一导航类目下仅需配置任意一个 Markdown 文件即可全局生效,未配置时将会使用[默认规则](../guide/conventional-routing.md#导航归类及生成)。
例如:
@@ -100,9 +110,9 @@ nav:
title: 名称
order: 1
# 单独配置二级导航名称
- parent: 父级名称
+ second: 父级名称
# 同时配置二级导航名称和顺序,order 越小越靠前,默认为 0
- parent:
+ second:
title: 父级名称
order: 1
---
diff --git a/docs/guide/conventional-routing.md b/docs/guide/conventional-routing.md
index 4c2729fea4..a4030fcd78 100644
--- a/docs/guide/conventional-routing.md
+++ b/docs/guide/conventional-routing.md
@@ -54,6 +54,8 @@ nav:
### 约定式二级导航 2.2.0+
+
+
同时,为了便于组织文档,dumi 还支持生成二级导航,使用起来也非常简单,以如下目录结构为例:
```bash
@@ -77,9 +79,9 @@ docs
---
nav:
# 单独设置二级导航名称
- parent: 移动端
+ second: 移动端
# 同时设置二级导航名称和顺序,order 越小越靠前,默认为 0
- parent:
+ second:
title: 移动端
order: 1
---
diff --git a/docs/theme/api.md b/docs/theme/api.md
index 7e9d0ed6da..31601af891 100644
--- a/docs/theme/api.md
+++ b/docs/theme/api.md
@@ -68,7 +68,7 @@ import { useFullSidebarData } from 'dumi';
const Example = () => {
const sidebar = useFullSidebarData();
// 返回值:Record
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L140
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L171
// 其他逻辑
};
@@ -86,7 +86,7 @@ import { useLocale } from 'dumi';
const Example = () => {
const locale = useLocale();
// 返回值:{ id: string; name: string; base: string } | { id: string; name: string; suffix: string }
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L121
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L152
// 其他逻辑
};
@@ -98,7 +98,9 @@ const Example = () => {
- 场景:定制导航栏时需要用到
- 用法:
-> 注意,由于后续会支持二级导航,该 hook 的返回数据结构可能会有些许调整
+:::info
+如果你是 dumi v2.2 发布之前的主题包开发者,建议更新导航栏组件对[二级导航数据](https://github.com/umijs/dumi/discussions/1618)的支持,并将主题包 `peerDependencies` 中的 dumi 版本设置为 `^2.2.0`,以便主题包用户使用[约定式二级导航](../guide/conventional-routing.md#约定式二级导航)特性
+:::
```ts
import { useNavData } from 'dumi';
@@ -106,7 +108,7 @@ import { useNavData } from 'dumi';
const Example = () => {
const nav = useNavData();
// 返回值:INavItem[]
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L126
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L157
// 其他逻辑
};
@@ -175,7 +177,7 @@ const Example = () => {
tabs,
} = useRouteMeta();
// 返回值:IRouteMeta
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L48
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L56
// 其他逻辑
};
@@ -224,7 +226,7 @@ import { useSidebarData } from 'dumi';
const Example = () => {
const sidebar = useSidebarData();
// 返回值:ISidebarGroup[]
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L140
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L171
// 其他逻辑
};
@@ -275,7 +277,7 @@ const Example = () => {
texts,
} = useTabMeta();
// 返回值:IRouteTabMeta
- // 类型定义:https://github.com/umijs/dumi/tree/master/src/client/theme-api/types.ts#L108
+ // 类型定义:https://github.com/umijs/dumi/blob/master/src/client/theme-api/types.ts#L135
// 其他逻辑
};
diff --git a/src/client/theme-api/context.ts b/src/client/theme-api/context.ts
index e6800e2320..221f306c6c 100644
--- a/src/client/theme-api/context.ts
+++ b/src/client/theme-api/context.ts
@@ -20,6 +20,10 @@ interface ISiteContext {
themeConfig: IThemeConfig;
loading: boolean;
setLoading: (status: boolean) => void;
+ /**
+ * private field, do not use it in your code
+ */
+ _2_level_nav_available: boolean;
}
export const SiteContext = createContext({
@@ -32,6 +36,7 @@ export const SiteContext = createContext({
themeConfig: {} as IThemeConfig,
loading: false,
setLoading: () => {},
+ _2_level_nav_available: true,
});
export const useSiteData = () => {
diff --git a/src/client/theme-api/types.ts b/src/client/theme-api/types.ts
index 56a66ebf9c..b0ca229e94 100644
--- a/src/client/theme-api/types.ts
+++ b/src/client/theme-api/types.ts
@@ -66,7 +66,7 @@ export interface IRouteMeta {
| {
title?: string;
order?: number;
- parent?: Omit;
+ second?: Omit;
};
group?: string | { title?: string; order?: number };
order?: number;
@@ -105,7 +105,7 @@ export interface IRouteMeta {
depth: number;
title: string;
/**
- * private field, will be removed in the future
+ * private field, do not use it in your code
*/
_debug_demo?: boolean;
}[];
@@ -143,7 +143,7 @@ export interface IRouteMeta {
};
}[];
/**
- * private field, will be removed in the future
+ * private field, do not use it in your code
*/
_atom_route?: boolean;
}
diff --git a/src/client/theme-api/useNavData.ts b/src/client/theme-api/useNavData.ts
index 6bf51e5e25..55104d36f3 100644
--- a/src/client/theme-api/useNavData.ts
+++ b/src/client/theme-api/useNavData.ts
@@ -35,7 +35,7 @@ function genNavItem(
export const useNavData = () => {
const locale = useLocale();
const routes = useLocaleDocRoutes();
- const { themeConfig } = useSiteData();
+ const { themeConfig, _2_level_nav_available: is2LevelNav } = useSiteData();
const sidebar = useFullSidebarData();
const sidebarDataComparer = useRouteDataComparer();
const [nav] = useState(() => {
@@ -65,8 +65,8 @@ export const useNavData = () => {
// convert sidebar data to nav data
.reduce>((ret, [link, groups]) => {
const [, parentPath, restPath] = link.match(/^(\/[^/]+)([^]+)?$/)!;
- const isNestedNav = Boolean(restPath);
- const [rootMeta, parentMeta] = Object.values(routes).reduce<
+ const isNestedNav = Boolean(restPath) && is2LevelNav;
+ const [firstMeta, secondMeta] = Object.values(routes).reduce<
{
title?: string;
order?: number;
@@ -80,7 +80,7 @@ export const useNavData = () => {
if (isNestedNav)
pickRouteSortMeta(
ret[1],
- 'nav.parent',
+ 'nav.second',
route.meta!.frontmatter,
);
}
@@ -90,26 +90,26 @@ export const useNavData = () => {
);
if (isNestedNav) {
- // fallback to use parent path as title
- parentMeta.title ??= parentPath
+ // fallback to use parent path as 1-level nav title
+ firstMeta.title ??= parentPath
.slice(1)
.replace(/^[a-z]/, (s) => s.toUpperCase());
// handle nested nav item as parent children
- const parent = (ret[parentPath] ??= genNavItem(
- parentMeta,
+ const second = (ret[parentPath] ??= genNavItem(
+ firstMeta,
groups,
parentPath,
));
- parent.children ??= [];
+ second.children ??= [];
ret[parentPath].children!.push(
- genNavItem(rootMeta, groups, link, groups[0].children[0].link),
+ genNavItem(secondMeta, groups, link, groups[0].children[0].link),
);
} else {
// handle root nav item
ret[link] = genNavItem(
- rootMeta,
+ firstMeta,
groups,
link,
groups[0].children[0].link,
diff --git a/src/client/theme-api/useSidebarData.ts b/src/client/theme-api/useSidebarData.ts
index 513b630a8a..5b82c32cce 100644
--- a/src/client/theme-api/useSidebarData.ts
+++ b/src/client/theme-api/useSidebarData.ts
@@ -24,9 +24,14 @@ const getLocaleClearPath = (routePath: string, locale: ILocalesConfig[0]) => {
/**
* get parent path from route path
*/
-function getRouteParentPath(path: string, meta: IRouteMeta) {
+function getRouteParentPath(
+ path: string,
+ { meta, is2LevelNav }: { meta: IRouteMeta; is2LevelNav: boolean },
+) {
const isIndexDocRoute =
- meta.frontmatter.filename?.endsWith('index.md') && !meta._atom_route;
+ meta.frontmatter.filename?.endsWith('index.md') &&
+ !meta._atom_route &&
+ is2LevelNav;
const paths = path
.split('/')
// strip end slash
@@ -38,8 +43,8 @@ function getRouteParentPath(path: string, meta: IRouteMeta) {
// least 1-level
1,
),
- // up to 2-level
- 2,
+ // up to 2-level when use conventional 2-level nav
+ is2LevelNav ? 2 : Infinity,
);
return paths.slice(0, sliceEnd).join('/');
@@ -51,7 +56,7 @@ function getRouteParentPath(path: string, meta: IRouteMeta) {
export const useFullSidebarData = () => {
const locale = useLocale();
const routes = useLocaleDocRoutes();
- const { themeConfig } = useSiteData();
+ const { themeConfig, _2_level_nav_available: is2LevelNav } = useSiteData();
const sidebarDataComparer = useRouteDataComparer<
ISidebarGroup | ISidebarItem
>();
@@ -74,7 +79,7 @@ export const useFullSidebarData = () => {
// a/b => /a/b (if route file is a/b/index.md)
// a/b/c => /a/b
const parentPath = `/${route.path!.replace(clearPath, (s) =>
- getRouteParentPath(s, route.meta!),
+ getRouteParentPath(s, { is2LevelNav, meta: route.meta! }),
)}`;
const { title, order } = pickRouteSortMeta(
{ order: 0 },
@@ -204,6 +209,7 @@ export const useTreeSidebarData = () => {
export const useSidebarData = () => {
const locale = useLocale();
const sidebar = useFullSidebarData();
+ const { _2_level_nav_available: is2LevelNav } = useSiteData();
const { pathname } = useLocation();
const meta = useRouteMeta();
const clearPath = getLocaleClearPath(pathname.slice(1), locale);
@@ -214,7 +220,9 @@ export const useSidebarData = () => {
// /en-US/a/b => /en-US/a
// /en-US/a/b/ => /en-US/a (also strip trailing /)
const parentPath = clearPath
- ? pathname.replace(clearPath, (s) => getRouteParentPath(s, meta))
+ ? pathname.replace(clearPath, (s) =>
+ getRouteParentPath(s, { is2LevelNav, meta }),
+ )
: pathname;
return parentPath ? sidebar[parentPath] : [];
diff --git a/src/client/theme-api/utils.ts b/src/client/theme-api/utils.ts
index f3d49a33c8..20c4934112 100644
--- a/src/client/theme-api/utils.ts
+++ b/src/client/theme-api/utils.ts
@@ -108,13 +108,13 @@ export const useRouteDataComparer = <
*/
export const pickRouteSortMeta = (
original: Partial>,
- field: 'nav' | 'nav.parent' | 'group',
+ field: 'nav' | 'nav.second' | 'group',
fm: IRouteMeta['frontmatter'],
) => {
const sub: IRouteMeta['frontmatter']['group'] =
- field === 'nav.parent'
+ field === 'nav.second'
? typeof fm.nav === 'object'
- ? fm.nav.parent
+ ? fm.nav.second
: {}
: fm[field];
diff --git a/src/client/theme-default/slots/Navbar/index.less b/src/client/theme-default/slots/Navbar/index.less
index b60f06b561..8b4d887c2d 100644
--- a/src/client/theme-default/slots/Navbar/index.less
+++ b/src/client/theme-default/slots/Navbar/index.less
@@ -78,8 +78,10 @@
}
}
- &[data-collapsed] > svg {
- transform: rotate(180deg);
+ @media @mobile {
+ &[data-collapsed] > svg {
+ transform: rotate(180deg);
+ }
}
@media @desktop {
@@ -122,7 +124,7 @@
padding: 0 18px;
color: @c-text-secondary;
font-size: 15px;
- line-height: 1.5;
+ line-height: 1.6;
text-align: left;
@media @mobile {
diff --git a/src/constants.ts b/src/constants.ts
index 76ce37a077..c4cb5887ee 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -23,3 +23,5 @@ export const PICKED_PKG_FIELDS = {
};
export const USELESS_TMP_FILES = ['tsconfig.json', 'typings.d.ts'];
+
+export const VERSION_2_LEVEL_NAV = '^2.2.0';
diff --git a/src/features/theme/index.ts b/src/features/theme/index.ts
index 25483a7b32..87b7dbaf83 100644
--- a/src/features/theme/index.ts
+++ b/src/features/theme/index.ts
@@ -3,12 +3,13 @@ import {
PICKED_PKG_FIELDS,
PREFERS_COLOR_ATTR,
THEME_PREFIX,
+ VERSION_2_LEVEL_NAV,
} from '@/constants';
import type { IApi } from '@/types';
import { parseModuleSync } from '@umijs/bundler-utils';
import fs from 'fs';
import path from 'path';
-import { deepmerge, lodash, resolve, winPath } from 'umi/plugin-utils';
+import { deepmerge, lodash, resolve, semver, winPath } from 'umi/plugin-utils';
import { safeExcludeInMFSU } from '../derivative';
import loadTheme, { IThemeLoadResult } from './loader';
@@ -57,6 +58,19 @@ function getModuleExports(modulePath: string) {
})[1];
}
+/**
+ * check if package dumi version is minor 2
+ */
+function checkMinor2ByPkg(pkg: IApi['pkg']) {
+ // for dumi local example project
+ if (pkg.name?.startsWith('@examples/')) return true;
+
+ const ver =
+ pkg.peerDependencies?.dumi || pkg.devDependencies?.dumi || '^2.0.0';
+
+ return semver.subset(ver, VERSION_2_LEVEL_NAV);
+}
+
export default (api: IApi) => {
// load default theme
const defaultThemeData = loadTheme(DEFAULT_THEME_PATH);
@@ -141,6 +155,21 @@ export default (api: IApi) => {
},
});
+ api.modifyAppData((memo) => {
+ // auto enable 2-level nav by declared dumi version, for existing projects compatibility
+ // ref: https://github.com/umijs/dumi/discussions/1618
+ memo._2LevelNavAvailable = checkMinor2ByPkg(api.pkg);
+
+ // always respect theme package declaration, for theme compatibility
+ if (pkgThemePath && !memo._2LevelNavAvailable) {
+ memo._2LevelNavAvailable = checkMinor2ByPkg(
+ require(path.join(pkgThemePath, 'package.json')),
+ );
+ }
+
+ return memo;
+ });
+
api.modifyConfig((memo) => {
// alias each component from local theme, as a part of final theme
if (localThemeData) {
@@ -306,6 +335,7 @@ export default function DumiContextWrapper() {
api.config.themeConfig,
),
)},
+ _2_level_nav_available: ${api.appData._2LevelNavAvailable},
}}>
{outlet}
diff --git a/suites/theme-mobile/package.json b/suites/theme-mobile/package.json
index 63b8208fb6..fafc16ffbd 100644
--- a/suites/theme-mobile/package.json
+++ b/suites/theme-mobile/package.json
@@ -35,7 +35,7 @@
"father-plugin-dumi-theme": "workspace:*"
},
"peerDependencies": {
- "dumi": "^2.0.0",
+ "dumi": "^2.2.0",
"react": ">=16.8",
"react-dom": ">=16.8"
},