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" },