diff --git a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts index 501ef6deba..cfb550ab9d 100644 --- a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts @@ -29,6 +29,7 @@ const DocFrontMatterSchema = Joi.object({ slug: Joi.string(), sidebar_label: Joi.string(), sidebar_position: Joi.number(), + sidebar_class_name: Joi.string(), tags: FrontMatterTagsSchema, pagination_label: Joi.string(), custom_edit_url: URISchema.allow('', null), diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 9c39f80738..5b64fefc12 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -30,6 +30,7 @@ declare module '@docusaurus/plugin-content-docs-types' { }; type PropsSidebarItemBase = { + className?: string; customProps?: Record; }; diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts index 95b7378122..b8c1dfe0d3 100644 --- a/packages/docusaurus-plugin-content-docs/src/props.ts +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -47,6 +47,7 @@ Available document ids are: type: 'link', label: sidebarLabel || item.label || title, href: permalink, + className: item.className, customProps: item.customProps, }; }; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts index d4659af1bf..7b759209d9 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebarItemsGenerator.ts @@ -30,6 +30,7 @@ export type CategoryMetadatasFile = { position?: number; collapsed?: boolean; collapsible?: boolean; + className?: string; // TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed? // This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/ @@ -44,6 +45,7 @@ const CategoryMetadatasFileSchema = Joi.object({ position: Joi.number(), collapsed: Joi.boolean(), collapsible: Joi.boolean(), + className: Joi.string(), }); // TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata @@ -177,6 +179,9 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ ...(doc.frontMatter.sidebar_label && { label: doc.frontMatter.sidebar_label, }), + ...(doc.frontMatter.sidebar_class_name && { + className: doc.frontMatter.sidebar_class_name, + }), ...(typeof doc.sidebarPosition !== 'undefined' && { position: doc.sidebarPosition, }), @@ -205,6 +210,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ const collapsible = categoryMetadatas?.collapsible ?? options.sidebarCollapsible; const collapsed = categoryMetadatas?.collapsed ?? options.sidebarCollapsed; + const className = categoryMetadatas?.className; return { type: 'category', @@ -213,6 +219,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ collapsed, collapsible, ...(typeof position !== 'undefined' && {position}), + ...(typeof className !== 'undefined' && {className}), }; } @@ -311,5 +318,5 @@ function sortSidebarItems( ['asc'], ); - return sortedSidebarItems.map(({position: _removed, ...item}) => item); + return sortedSidebarItems.map(({position, ...item}) => item); } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index f27a0fa561..a14bc8a726 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -40,6 +40,7 @@ type SidebarItemCategoryJSON = SidebarItemBase & { items: SidebarItemJSON[]; collapsed?: boolean; collapsible?: boolean; + className?: string; }; type SidebarItemAutogeneratedJSON = SidebarItemBase & { @@ -100,8 +101,7 @@ function assertItem( keys: K[], ): asserts item is Record { const unknownKeys = Object.keys(item).filter( - // @ts-expect-error: key is always string - (key) => !keys.includes(key as string) && key !== 'type', + (key) => !keys.includes(key as K) && key !== 'type', ); if (unknownKeys.length) { @@ -121,6 +121,7 @@ function assertIsCategory( 'label', 'collapsed', 'collapsible', + 'className', 'customProps', ]); if (typeof item.label !== 'string') { @@ -150,6 +151,14 @@ function assertIsCategory( `Error loading ${JSON.stringify(item)}: "collapsible" must be a boolean.`, ); } + if ( + typeof item.className !== 'undefined' && + typeof item.className !== 'string' + ) { + throw new Error( + `Error loading ${JSON.stringify(item)}: "className" must be a string.`, + ); + } } function assertIsAutogenerated( @@ -173,24 +182,33 @@ function assertIsAutogenerated( function assertIsDoc( item: Record, ): asserts item is SidebarItemDoc { - assertItem(item, ['id', 'label', 'customProps']); + assertItem(item, ['id', 'label', 'className', 'customProps']); if (typeof item.id !== 'string') { throw new Error( `Error loading ${JSON.stringify(item)}: "id" must be a string.`, ); } - if (item.label && typeof item.label !== 'string') { + if (typeof item.label !== 'undefined' && typeof item.label !== 'string') { throw new Error( `Error loading ${JSON.stringify(item)}: "label" must be a string.`, ); } + + if ( + typeof item.className !== 'undefined' && + typeof item.className !== 'string' + ) { + throw new Error( + `Error loading ${JSON.stringify(item)}: "className" must be a string.`, + ); + } } function assertIsLink( item: Record, ): asserts item is SidebarItemLink { - assertItem(item, ['href', 'label', 'customProps']); + assertItem(item, ['href', 'label', 'className', 'customProps']); if (typeof item.href !== 'string') { throw new Error( `Error loading ${JSON.stringify(item)}: "href" must be a string.`, @@ -201,6 +219,14 @@ function assertIsLink( `Error loading ${JSON.stringify(item)}: "label" must be a string.`, ); } + if ( + typeof item.className !== 'undefined' && + typeof item.className !== 'string' + ) { + throw new Error( + `Error loading ${JSON.stringify(item)}: "className" must be a string.`, + ); + } } /** diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index f85e002a4e..e62bf2b3c2 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -105,6 +105,7 @@ export type PluginOptions = MetadataOptions & }; export type SidebarItemBase = { + className?: string; customProps?: Record; }; @@ -217,6 +218,7 @@ export type DocFrontMatter = { slug?: string; sidebar_label?: string; sidebar_position?: number; + sidebar_class_name?: string; pagination_label?: string; custom_edit_url?: string | null; parse_number_prefixes?: boolean; diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx index fd5f3fe331..a517f6d08c 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebar/index.tsx @@ -80,7 +80,7 @@ function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) { !isAnnouncementBarClosed && showAnnouncementBar, })}>
    - +
{hideableSidebar && } @@ -99,6 +99,7 @@ const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent = ({ items={sidebar} activePath={path} onItemClick={() => toggleSidebar()} + level={1} /> ); diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx index 637f0d1846..8c769004fe 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebarItem/index.tsx @@ -100,9 +100,10 @@ function DocSidebarItemCategory({ item, onItemClick, activePath, + level, ...props }: Props & {item: PropSidebarItemCategory}) { - const {items, label, collapsible} = item; + const {items, label, collapsible, className} = item; const isActive = isActiveSidebarItem(item, activePath); @@ -123,10 +124,12 @@ function DocSidebarItemCategory({
  • {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  • @@ -164,15 +168,18 @@ function DocSidebarItemLink({ item, onItemClick, activePath, + level, ...props }: Props & {item: PropSidebarItemLink}) { - const {href, label} = item; + const {href, label, className} = item; const isActive = isActiveSidebarItem(item, activePath); return (
  • void; + readonly level: number; readonly tabIndex?: number; }; diff --git a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts index 7481eba871..047b40be5c 100644 --- a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts +++ b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts @@ -50,9 +50,13 @@ export const ThemeClassNames = { docSidebarMenu: 'theme-doc-sidebar-menu', docSidebarItemCategory: 'theme-doc-sidebar-item-category', docSidebarItemLink: 'theme-doc-sidebar-item-link', + docSidebarItemCategoryLevel: (level: number) => + `theme-doc-sidebar-item-category-level-${level}` as const, + docSidebarItemLinkLevel: (level: number) => + `theme-doc-sidebar-item-link-level-${level}` as const, // TODO add other stable classNames here }, blog: { // TODO add other stable classNames here }, -}; +} as const; diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index 3d1907903b..6305fe410b 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -10,6 +10,7 @@ module.exports = { { type: 'doc', id: 'index', + className: 'red', label: 'Index', }, { @@ -23,9 +24,16 @@ module.exports = { label: 'Huge sidebar category', items: generateHugeSidebarItems(4), }, + { + type: 'link', + label: 'External link', + href: 'https://github.com/facebook/docusaurus', + className: 'red', + }, { type: 'category', label: 'TOC tests', + className: 'red', items: [ { type: 'autogenerated', diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index b3ec5da704..942cd7ef2e 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -247,6 +247,7 @@ Accepted fields: | `pagination_label` | `string` | `sidebar_label` or `title` | The text used in the document next/previous buttons for this document. | | `sidebar_label` | `string` | `title` | The text shown in the document sidebar for this document. | | `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadatas](/docs/sidebar#autogenerated-sidebar-metadatas). | +| `sidebar_class_name` | `string` | `undefined` | Gives the corresponding sidebar label a special class name when using autogenerated sidebars. | | `hide_title` | `boolean` | `false` | Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a Markdown title at the top of your document. | | `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. | | `toc_min_heading_level` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. | diff --git a/website/docs/guides/docs/sidebar.md b/website/docs/guides/docs/sidebar.md index 47aecb43f2..ac49def5ae 100644 --- a/website/docs/guides/docs/sidebar.md +++ b/website/docs/guides/docs/sidebar.md @@ -173,6 +173,7 @@ type SidebarItemDoc = type: 'doc'; id: string; label: string; // Sidebar label text + className?: string; // Class name for sidebar label } // Shorthand syntax @@ -244,6 +245,7 @@ type SidebarItemLink = { type: 'link'; label: string; href: string; + className?: string; }; ``` @@ -282,6 +284,7 @@ type SidebarItemCategory = { type: 'category'; label: string; // Sidebar label text. items: SidebarItem[]; // Array of sidebar items. + className?: string; // Category options: collapsible: boolean; // Set the category to be collapsible @@ -519,7 +522,8 @@ This is the easy tutorial! ```json title="docs/tutorials/_category_.json" { "label": "Tutorial", - "position": 3 + "position": 3, + "className": "red" } ``` diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 5b8f461dda..cd96243272 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -146,3 +146,7 @@ div[class^='announcementBar_'] { ); font-weight: bold; } + +.red > a { + color: red; +}