feat(v2): redesign mobile UX: inline TOC + doc sidebar in main menu (#4273)

* feat(v2): mobile TOC

* Bug fixes and various improvements

* Redesign

* extract TOCCollapsible component

* TS improvements

* Assign sidebar name directly to the doc route => no need for either permalinkToSidebar or GlobalData

* revert changes to useWindowSize, fix FOUC issues

* extract DocSidebarDesktop component

* remove now useless menu infima classes

* TOCHeadings => rename + remove unused onClick prop

* Extract DocSidebarItem

* minor renaming

* replace GlobalData usage by a React teleport system to render in the navbar mobile sidebar menu directly from the DocPage component

* useWindowSize => simulate SSR size in dev to make FOUC issues more obvious

* fix remaining sidebar layout shift

* update docs snapshots

* remove unused code translations

* remove unused code translations

* fix minor update-code-translations bug

* Add more build-size paths to watch

* Restyle back button

* Add  missing`menu` class

* extract useShallowMemoizedObject

* fix routes tests + better routes formatting

* use Translate api for labels

* use Translate api for labels

* Update translations

* Improve dark mode support for back button

* Merge branch 'master' into lex111/inline-color-code

# Conflicts:
#	packages/core/dist/css/default-dark/default-dark-rtl.min.css
#	packages/core/dist/css/default-dark/default-dark.min.css
#	packages/core/dist/css/default/default-rtl.min.css
#	packages/core/dist/css/default/default.min.css

* replace useCollapse by new useCollapsible

* Cleanup and use clean-btn for TOCCollapsible button

* Make TOC links clickable over full width

* Cleanup

* fix uncollapsible sidebar that can be collapsed + create <Collapsible> component

* dependency array typo

* rollback sidebars community commit typo

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Alexey Pyltsyn 2021-07-09 17:50:38 +03:00 committed by GitHub
parent f03479f69e
commit 9536ef900d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1006 additions and 633 deletions

View file

@ -26,7 +26,7 @@ jobs:
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
build-script: 'build:v2:en'
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/build/index.html,website/build/blog/**/introducing-docusaurus/*,website/build/docs/index.html,website/build/docs/introduction/index.html,website/build/docs-tests/index.html,website/build/docs-tests/standalone/index.html}'
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/build/index.html,website/build/blog/index.html,website/build/blog/**/introducing-docusaurus/*,website/build/docs/index.html,website/build/docs/installation/index.html,website/build/docs-tests/index.html,website/build/docs-tests/standalone/index.html}'
strip-hash: '\.([^;]\w{7})\.'
minimum-change-threshold: 30
compression: 'none'

View file

@ -456,11 +456,6 @@ Object {
]
}
]
},
\\"permalinkToSidebar\\": {
\\"/docs/foo/bar\\": \\"docs\\",
\\"/docs/foo/bazSlug.html\\": \\"docs\\",
\\"/docs/\\": \\"docs\\"
}
}",
}
@ -575,6 +570,7 @@ Array [
"content": "@site/docs/hello.md",
},
"path": "/docs/",
"sidebar": "docs",
},
Object {
"component": "@theme/DocItem",
@ -591,6 +587,7 @@ Array [
"content": "@site/docs/foo/bar.md",
},
"path": "/docs/foo/bar",
"sidebar": "docs",
},
Object {
"component": "@theme/DocItem",
@ -599,6 +596,7 @@ Array [
"content": "@site/docs/foo/baz.md",
},
"path": "/docs/foo/bazSlug.html",
"sidebar": "docs",
},
Object {
"component": "@theme/DocItem",
@ -883,9 +881,6 @@ Object {
\\"href\\": \\"/community/team\\"
}
]
},
\\"permalinkToSidebar\\": {
\\"/community/team\\": \\"version-1.0.0/community\\"
}
}",
"version-current-metadata-prop-751.json": "{
@ -902,9 +897,6 @@ Object {
\\"href\\": \\"/community/next/team\\"
}
]
},
\\"permalinkToSidebar\\": {
\\"/community/next/team\\": \\"community\\"
}
}",
}
@ -968,6 +960,7 @@ Array [
"content": "@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
},
"path": "/community/next/team",
"sidebar": "community",
},
],
},
@ -987,6 +980,7 @@ Array [
"content": "@site/community_versioned_docs/version-1.0.0/team.md",
},
"path": "/community/team",
"sidebar": "version-1.0.0/community",
},
],
},
@ -1439,11 +1433,6 @@ Object {
]
}
]
},
\\"permalinkToSidebar\\": {
\\"/docs/1.0.0/foo/barSlug\\": \\"version-1.0.0/docs\\",
\\"/docs/1.0.0/foo/baz\\": \\"version-1.0.0/docs\\",
\\"/docs/1.0.0/\\": \\"version-1.0.0/docs\\"
}
}",
"version-1-0-1-metadata-prop-e87.json": "{
@ -1479,10 +1468,6 @@ Object {
]
}
]
},
\\"permalinkToSidebar\\": {
\\"/docs/foo/bar\\": \\"version-1.0.1/docs\\",
\\"/docs/\\": \\"version-1.0.1/docs\\"
}
}",
"version-current-metadata-prop-751.json": "{
@ -1518,10 +1503,6 @@ Object {
]
}
]
},
\\"permalinkToSidebar\\": {
\\"/docs/next/foo/barSlug\\": \\"docs\\",
\\"/docs/next/\\": \\"docs\\"
}
}",
"version-with-slugs-metadata-prop-2bf.json": "{
@ -1545,9 +1526,6 @@ Object {
]
}
]
},
\\"permalinkToSidebar\\": {
\\"/docs/withSlugs/rootAbsoluteSlug\\": \\"version-1.0.1/docs\\"
}
}",
}
@ -1714,6 +1692,7 @@ Array [
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
},
"path": "/docs/1.0.0/",
"sidebar": "version-1.0.0/docs",
},
Object {
"component": "@theme/DocItem",
@ -1722,6 +1701,7 @@ Array [
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
},
"path": "/docs/1.0.0/foo/barSlug",
"sidebar": "version-1.0.0/docs",
},
Object {
"component": "@theme/DocItem",
@ -1730,6 +1710,7 @@ Array [
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
},
"path": "/docs/1.0.0/foo/baz",
"sidebar": "version-1.0.0/docs",
},
],
},
@ -1749,6 +1730,7 @@ Array [
"content": "@site/docs/hello.md",
},
"path": "/docs/next/",
"sidebar": "docs",
},
Object {
"component": "@theme/DocItem",
@ -1765,6 +1747,7 @@ Array [
"content": "@site/docs/foo/bar.md",
},
"path": "/docs/next/foo/barSlug",
"sidebar": "docs",
},
Object {
"component": "@theme/DocItem",
@ -1824,6 +1807,7 @@ Array [
"content": "@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
},
"path": "/docs/withSlugs/rootAbsoluteSlug",
"sidebar": "version-1.0.1/docs",
},
Object {
"component": "@theme/DocItem",
@ -1883,6 +1867,7 @@ Array [
"content": "@site/versioned_docs/version-1.0.1/hello.md",
},
"path": "/docs/",
"sidebar": "version-1.0.1/docs",
},
Object {
"component": "@theme/DocItem",
@ -1891,6 +1876,7 @@ Array [
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
},
"path": "/docs/foo/bar",
"sidebar": "version-1.0.1/docs",
},
],
},

View file

@ -151,7 +151,6 @@ Object {
],
"isLast": true,
"mainDocId": "",
"permalinkToSidebar": Object {},
"routePriority": undefined,
"sidebarFilePath": "any",
"sidebars": Object {
@ -292,7 +291,6 @@ Object {
],
"isLast": true,
"mainDocId": "",
"permalinkToSidebar": Object {},
"routePriority": undefined,
"sidebarFilePath": "any",
"sidebars": Object {
@ -433,7 +431,6 @@ Object {
],
"isLast": true,
"mainDocId": "",
"permalinkToSidebar": Object {},
"routePriority": undefined,
"sidebarFilePath": "any",
"sidebars": Object {

View file

@ -105,9 +105,6 @@ Entries created:
`version-${kebabCase(version.versionName)}-metadata-prop`,
);
expect(versionMetadataProp.docsSidebars).toEqual(toSidebarsProp(version));
expect(versionMetadataProp.permalinkToSidebar).toEqual(
version.permalinkToSidebar,
);
},
expectSnapshot: () => {

View file

@ -40,7 +40,6 @@ function createSampleVersion(
versionLabel: `${version.versionName} label`,
versionPath: '/docs/',
mainDocId: '',
permalinkToSidebar: {},
routePriority: undefined,
sidebarFilePath: 'any',
isLast: true,

View file

@ -37,7 +37,6 @@ import {
DocFile,
DocsMarkdownOption,
} from './types';
import {PermalinkToSidebar} from '@docusaurus/plugin-content-docs-types';
import {RuleSetRule} from 'webpack';
import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants';
@ -224,14 +223,6 @@ export default function pluginContentDocs(
// sort to ensure consistent output for tests
docs.sort((a, b) => a.id.localeCompare(b.id));
// TODO really useful? replace with global state logic?
const permalinkToSidebar: PermalinkToSidebar = {};
Object.values(docs).forEach((doc) => {
if (doc.sidebar) {
permalinkToSidebar[doc.permalink] = doc.sidebar;
}
});
// The "main doc" is the "version entry point"
// We browse this doc by clicking on a version:
// - the "home" doc (at '/docs/')
@ -256,7 +247,6 @@ export default function pluginContentDocs(
...versionMetadata,
mainDocId: getMainDoc().unversionedId,
sidebars,
permalinkToSidebar,
docs: docs.map(addNavData),
};
}
@ -300,30 +290,36 @@ export default function pluginContentDocs(
JSON.stringify(metadataItem, null, 2),
);
return {
const docRoute: RouteConfig = {
path: metadataItem.permalink,
component: docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
},
// Because the parent (DocPage) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state)
...(metadataItem.sidebar && {
sidebar: metadataItem.sidebar,
}),
};
return docRoute;
}),
);
return routes.sort((a, b) => a.path.localeCompare(b.path));
};
async function doCreateVersionRoutes(loadedVersion: LoadedVersion) {
async function doCreateVersionRoutes(
loadedVersion: LoadedVersion,
): Promise<void> {
const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion);
const versionMetadataPropPath = await createData(
`${docuHash(
`version-${loadedVersion.versionName}-metadata-prop`,
)}.json`,
JSON.stringify(
toVersionMetadataProp(pluginId, loadedVersion),
null,
2,
),
JSON.stringify(versionMetadata, null, 2),
);
addRoute({
@ -341,7 +337,9 @@ export default function pluginContentDocs(
});
}
async function createVersionRoutes(loadedVersion: LoadedVersion) {
async function createVersionRoutes(
loadedVersion: LoadedVersion,
): Promise<void> {
try {
return await doCreateVersionRoutes(loadedVersion);
} catch (e) {

View file

@ -10,10 +10,6 @@
declare module '@docusaurus/plugin-content-docs-types' {
import type {VersionBanner} from './types';
export type PermalinkToSidebar = {
[permalink: string]: string;
};
export type PropVersionMetadata = {
pluginId: string;
version: string;
@ -21,7 +17,6 @@ declare module '@docusaurus/plugin-content-docs-types' {
banner: VersionBanner;
isLast: boolean;
docsSidebars: PropSidebars;
permalinkToSidebar: PermalinkToSidebar;
};
type PropsSidebarItemBase = {
@ -60,6 +55,7 @@ declare module '@theme/DocItem' {
readonly component: () => JSX.Element;
readonly exact: boolean;
readonly path: string;
readonly sidebar?: string;
};
export type FrontMatter = {

View file

@ -77,6 +77,5 @@ export function toVersionMetadataProp(
banner: loadedVersion.versionBanner,
isLast: loadedVersion.isLast,
docsSidebars: toSidebarsProp(loadedVersion),
permalinkToSidebar: loadedVersion.permalinkToSidebar,
};
}

View file

@ -239,7 +239,6 @@ export type LoadedVersion = VersionMetadata & {
mainDocId: string;
docs: DocMetadata[];
sidebars: Sidebars;
permalinkToSidebar: Record<string, string>;
};
export type LoadedContent = {

View file

@ -8,37 +8,25 @@
import React, {ReactNode} from 'react';
import renderRoutes from '@docusaurus/renderRoutes';
import NotFound from '@theme/NotFound';
import DocSidebar from '@theme/DocSidebar';
import MDXComponents from '@theme/MDXComponents';
import Layout from '@theme/Layout';
import {MDXProvider} from '@mdx-js/react';
import {matchPath} from '@docusaurus/router';
import type {Props} from '@theme/DocPage';
import type {DocumentRoute} from '@theme/DocItem';
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types';
type DocPageContentProps = {
readonly currentDocRoute: DocumentRoute;
readonly versionMetadata: PropVersionMetadata;
readonly children: ReactNode;
};
function DocPageContent({
currentDocRoute,
versionMetadata,
versionMetadata: _versionMetadata,
children,
}: DocPageContentProps): JSX.Element {
const {permalinkToSidebar, docsSidebars} = versionMetadata;
const sidebarName = permalinkToSidebar[currentDocRoute.path];
const sidebar = docsSidebars[sidebarName];
return (
<Layout title="Doc page" description="My Doc page">
<div className="d-flex vh-100">
{sidebar && (
<div role="complementary">
<DocSidebar key={sidebarName} sidebar={sidebar} />
</div>
)}
<main className="w-100 align-items-center overflow-auto p-5">
<MDXProvider components={MDXComponents}>{children}</MDXProvider>
</main>
@ -60,9 +48,7 @@ function DocPage(props: Props): JSX.Element {
return <NotFound {...props} />;
}
return (
<DocPageContent
currentDocRoute={currentDocRoute}
versionMetadata={versionMetadata}>
<DocPageContent versionMetadata={versionMetadata}>
{renderRoutes(docRoutes)}
</DocPageContent>
);

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "بحث",
"theme.SearchPage.inputPlaceholder": "اكتب ما تبحث عنه هنا",
"theme.SearchPage.noResultsText": "لم يتم العثور على نتائج",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "التنقل في صفحة قائمة المدونة",
"theme.blog.paginator.newerEntries": "إدخالات أحدث",
"theme.blog.paginator.olderEntries": "إدخالات أقدم",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "طي الشريط الجانبي",
"theme.docs.sidebar.expandButtonAriaLabel": "توسيع الشريط الجانبي",
"theme.docs.sidebar.expandButtonTitle": "توسيع الشريط الجانبي",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "إغلاق القائمة",
"theme.docs.sidebar.responsiveOpenButtonLabel": "فتح القائمة",
"theme.docs.versions.latestVersionLinkLabel": "احدث اصدار",
"theme.docs.versions.latestVersionSuggestionLabel": "للحصول على أحدث الوثائق، راجع {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "هذه هي وثائق {siteTitle} {versionLabel}، التي لم تعد تتم صيانتها بشكل نشط.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " في {date}",
"theme.lastUpdated.byUser": " بواسطة {user}",
"theme.lastUpdated.lastUpdatedAtBy": "آخر تحديث{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "الوسوم:",
"theme.tags.tagsPageLink": "عرض كل الوسوم",
"theme.tags.tagsPageTitle": "الوسوم"

View file

@ -41,6 +41,8 @@
"theme.SearchPage.inputPlaceholder___DESCRIPTION": "The placeholder for search page input",
"theme.SearchPage.noResultsText": "No results were found",
"theme.SearchPage.noResultsText___DESCRIPTION": "The paragraph for empty search result",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.TOCCollapsible.toggleButtonLabel___DESCRIPTION": "The label used by the button on the collapsible TOC component",
"theme.blog.paginator.navAriaLabel": "Blog list page navigation",
"theme.blog.paginator.navAriaLabel___DESCRIPTION": "The ARIA label for the blog pagination",
"theme.blog.paginator.newerEntries": "Newer Entries",
@ -83,16 +85,10 @@
"theme.docs.sidebar.expandButtonAriaLabel___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
"theme.docs.sidebar.expandButtonTitle___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.navAriaLabel___DESCRIPTION": "The ARIA label for documentation menu",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Close menu",
"theme.docs.sidebar.responsiveCloseButtonLabel___DESCRIPTION": "The ARIA label for close button of mobile doc sidebar",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Open menu",
"theme.docs.sidebar.responsiveOpenButtonLabel___DESCRIPTION": "The ARIA label for open button of mobile doc sidebar",
"theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.docs.versions.latestVersionLinkLabel___DESCRIPTION": "The label used for the latest version suggestion link label",
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.latestVersionSuggestionLabel___DESCRIPTION": "The label userd to tell the user that he's browsing an unmaintained doc version",
"theme.docs.versions.latestVersionSuggestionLabel___DESCRIPTION": "The label used to tell the user to check the latest version",
"theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
"theme.docs.versions.unmaintainedVersionLabel___DESCRIPTION": "The label used to tell the user that he's browsing an unmaintained doc version",
"theme.docs.versions.unreleasedVersionLabel": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
@ -103,6 +99,8 @@
"theme.lastUpdated.byUser___DESCRIPTION": "The words used to describe by who the page has been last updated",
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
"theme.lastUpdated.lastUpdatedAtBy___DESCRIPTION": "The sentence used to display when a page has been last updated, and by who",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel___DESCRIPTION": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",
"theme.tags.tagsListLabel": "Tags:",
"theme.tags.tagsListLabel___DESCRIPTION": "The label alongside a tag list",
"theme.tags.tagsPageLink": "View All Tags",

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "সার্চ",
"theme.SearchPage.inputPlaceholder": "আপনার অনুসন্ধান এখানে টাইপ করুন",
"theme.SearchPage.noResultsText": "কোন ফলাফল পাওয়া যায়নি",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "ব্লগ তালিকা পেজ নেভিগেশন",
"theme.blog.paginator.newerEntries": "নতুন এন্ট্রি",
"theme.blog.paginator.olderEntries": "পুরানো এন্ট্রি",
@ -41,8 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "সাইডবারটি সঙ্কুচিত করুন",
"theme.docs.sidebar.expandButtonAriaLabel": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.expandButtonTitle": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.responsiveCloseButtonLabel": "মেনু বন্ধ করুন",
"theme.docs.sidebar.responsiveOpenButtonLabel": "মেনু খুলুন",
"theme.docs.versions.latestVersionLinkLabel": "লেটেস্ট ভার্সন",
"theme.docs.versions.latestVersionSuggestionLabel": "আপ-টু-ডেট ডকুমেন্টেশনের জন্য, {latestVersionLink} ({versionLabel}) দেখুন।",
"theme.docs.versions.unmaintainedVersionLabel": "এটি {siteTitle} {versionLabel} এর জন্যে ডকুমেন্টেশন, যা আর সক্রিয়ভাবে রক্ষণাবেক্ষণ করা হয় না।",
@ -50,6 +49,7 @@
"theme.lastUpdated.atDate": " {date} তারিখে",
"theme.lastUpdated.byUser": "{user} দ্বারা",
"theme.lastUpdated.lastUpdatedAtBy": "সর্বশেষ সংষ্করণ{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "ট্যাগ্স:",
"theme.tags.tagsPageLink": "সমস্ত ট্যাগ্স দেখুন",
"theme.tags.tagsPageTitle": "ট্যাগ্স"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Søg",
"theme.SearchPage.inputPlaceholder": "Indtast din søgning her",
"theme.SearchPage.noResultsText": "Ingen resultater fundet",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Blogoversigt navigation",
"theme.blog.paginator.newerEntries": "Nyere indslag",
"theme.blog.paginator.olderEntries": "Tidligere indslag",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Sammenlæg sidenavigation",
"theme.docs.sidebar.expandButtonAriaLabel": "Udvid sidenavigation",
"theme.docs.sidebar.expandButtonTitle": "Udvid sidenavigation",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Luk menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Åben menu",
"theme.docs.versions.latestVersionLinkLabel": "seneste version",
"theme.docs.versions.latestVersionSuggestionLabel": "For seneste dokumentation, se {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Dette er dokumentationen for {siteTitle} {versionLabel}, som ikke længere bliver aktivt vedligeholdt.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " den {date}",
"theme.lastUpdated.byUser": " af {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Senest opdateret{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Tags:",
"theme.tags.tagsPageLink": "Se alle Tags",
"theme.tags.tagsPageTitle": "Tags"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Suche",
"theme.SearchPage.inputPlaceholder": "Geben Sie hier Ihre Suche ein",
"theme.SearchPage.noResultsText": "Es wurden keine Ergebnisse gefunden",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
"theme.blog.paginator.newerEntries": "Neuere Einträge",
"theme.blog.paginator.olderEntries": "Ältere Einträge",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Menü schließen",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Menü öffenen",
"theme.docs.versions.latestVersionLinkLabel": "letzte Version",
"theme.docs.versions.latestVersionSuggestionLabel": "Für die aktuellste Dokumentation bitte auf {latestVersionLink} ({versionLabel}) gehen.",
"theme.docs.versions.unmaintainedVersionLabel": "Das ist die Dokumentation für {siteTitle} {versionLabel} und wird nicht weiter gewartet.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " am {date}",
"theme.lastUpdated.byUser": " von {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Letztes Update{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Tags:",
"theme.tags.tagsPageLink": "Alle Tags anzeigen",
"theme.tags.tagsPageTitle": "Tags"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Buscar",
"theme.SearchPage.inputPlaceholder": "Escribe tu búsqueda aquí",
"theme.SearchPage.noResultsText": "No se encontraron resultados",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Navegación por la página de la lista de blogs ",
"theme.blog.paginator.newerEntries": "Entradas más recientes",
"theme.blog.paginator.olderEntries": "Entradas más antiguas",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Cerrar menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Abrir menu",
"theme.docs.versions.latestVersionLinkLabel": "última versión",
"theme.docs.versions.latestVersionSuggestionLabel": "Para documentación actualizada, ver {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Está es documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " en {date}",
"theme.lastUpdated.byUser": " por {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Última actualización{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Etiquetas:",
"theme.tags.tagsPageLink": "Ver Todas las Etiquetas",
"theme.tags.tagsPageTitle": "Etiquetas"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "جستجو",
"theme.SearchPage.inputPlaceholder": "عبارت مورد نظر را اینجا بنویسید",
"theme.SearchPage.noResultsText": "هیچ نتیجه ای پیدا نشد",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "کنترل لیست صفحه وبسایت",
"theme.blog.paginator.newerEntries": "مطالب جدیدتر",
"theme.blog.paginator.olderEntries": "مطالب قدیمی تر",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
"theme.docs.sidebar.expandButtonAriaLabel": "بزرگ کردن نوار کناری",
"theme.docs.sidebar.expandButtonTitle": "بزرگ کردن نوار کناری",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "بستن منو",
"theme.docs.sidebar.responsiveOpenButtonLabel": "باز کردن منو",
"theme.docs.versions.latestVersionLinkLabel": "آخرین نسخه",
"theme.docs.versions.latestVersionSuggestionLabel": "برای دیدن آخرین نسخه، {latestVersionLink} ({versionLabel}) را ببینید.",
"theme.docs.versions.unmaintainedVersionLabel": "نسخه {siteTitle} {versionLabel} دیگر بروزرسانی نمی شود.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " در تاریخ {date}",
"theme.lastUpdated.byUser": " توسط {user}",
"theme.lastUpdated.lastUpdatedAtBy": "آخرین به روزرسانی{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": ":برچسب ها",
"theme.tags.tagsPageLink": "مشاهده تمام برچسب ها",
"theme.tags.tagsPageTitle": "برچسب ها"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Maghanap",
"theme.SearchPage.inputPlaceholder": "I-type and inyong hinahanap dito",
"theme.SearchPage.noResultsText": "Walang resultang nahanap",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Nabegasyón para sa pahina na listahan ng blog",
"theme.blog.paginator.newerEntries": "Mas bagong mga éntri",
"theme.blog.paginator.olderEntries": "Mas lumang mga éntri",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Itupî ang sidebar",
"theme.docs.sidebar.expandButtonAriaLabel": "Palakihin ang sidebar",
"theme.docs.sidebar.expandButtonTitle": "Palakihin ang sidebar",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Isara ang menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Buksan ang menu",
"theme.docs.versions.latestVersionLinkLabel": "pinakahuling bersiyón",
"theme.docs.versions.latestVersionSuggestionLabel": "Para sa up-to-date na dokumentasyón, tingnan ang {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ito ay dokumentasyón para sa {siteTitle} {versionLabel} na hindi na aktibong mine-maintain.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " noong {date}",
"theme.lastUpdated.byUser": " ni {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Huling inapdeyt{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Mga Tag:",
"theme.tags.tagsPageLink": "Tingnan Lahat ng mga Tag",
"theme.tags.tagsPageTitle": "Mga Tag"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Chercher",
"theme.SearchPage.inputPlaceholder": "Tapez votre recherche ici",
"theme.SearchPage.noResultsText": "Aucun résultat trouvé",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Pagination de la liste des articles du blog",
"theme.blog.paginator.newerEntries": "Nouvelles entrées",
"theme.blog.paginator.olderEntries": "Anciennes entrées",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Fermer le menu latéral",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Ouvrir le menu latéral",
"theme.docs.versions.latestVersionLinkLabel": "dernière version",
"theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " le {date}",
"theme.lastUpdated.byUser": " par {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Dernière mise à jour{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Tags :",
"theme.tags.tagsPageLink": "Voir tous les tags",
"theme.tags.tagsPageTitle": "Tags"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "חיפוש",
"theme.SearchPage.inputPlaceholder": "הקלד כאן לחיפוש",
"theme.SearchPage.noResultsText": "לא נמצאו תוצאות",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "רשימת דפי הבלוג",
"theme.blog.paginator.newerEntries": "הכי חדש",
"theme.blog.paginator.olderEntries": "ישן יותר",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "סגור",
"theme.docs.sidebar.expandButtonAriaLabel": "פתח",
"theme.docs.sidebar.expandButtonTitle": "פתח",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "סגור תפריט",
"theme.docs.sidebar.responsiveOpenButtonLabel": "פתח תפריט",
"theme.docs.versions.latestVersionLinkLabel": "גרסא אחרונה",
"theme.docs.versions.latestVersionSuggestionLabel": "לדוקומנטאציה עדכנית, ראה {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "דוקומנטאציה זו {siteTitle} {versionLabel}, כבר לא נתמכת.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " בתאריך {date}",
"theme.lastUpdated.byUser": " על ידי {user}",
"theme.lastUpdated.lastUpdatedAtBy": "עודכן{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "תגיות:",
"theme.tags.tagsPageLink": "כל התגיות",
"theme.tags.tagsPageTitle": "תגיות"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "खोज करें",
"theme.SearchPage.inputPlaceholder": "अपनी खोज यहाँ टाइप करें",
"theme.SearchPage.noResultsText": "कोई परिणाम नहीं मिलें",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "ब्लॉग सूची पेज नेविगेशन",
"theme.blog.paginator.newerEntries": "नए एंट्रीज़",
"theme.blog.paginator.olderEntries": "पुराने एंट्रीज़",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "साइडबार बंद करें",
"theme.docs.sidebar.expandButtonAriaLabel": "साइडबार खोलें",
"theme.docs.sidebar.expandButtonTitle": "साइडबार खोलें",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "मेन्यू बंद करें",
"theme.docs.sidebar.responsiveOpenButtonLabel": "मेन्यू खोलें",
"theme.docs.versions.latestVersionLinkLabel": "सबसे नया वर्जन",
"theme.docs.versions.latestVersionSuggestionLabel": "अप-टू-डेट डॉक्यूमेंटेशन के लिए {latestVersionLink} ({versionLabel}) देखें।",
"theme.docs.versions.unmaintainedVersionLabel": "यह {siteTitle} {versionLabel} के लिए डॉक्यूमेंटेशन है, जिसे अब सक्रिय रूप से नहीं बनाए रखा गया है।",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " {date} पर",
"theme.lastUpdated.byUser": " {user} द्वारा",
"theme.lastUpdated.lastUpdatedAtBy": "आखरी अपडेट{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "टैग:",
"theme.tags.tagsPageLink": "सारे टैग देखें",
"theme.tags.tagsPageTitle": "टैग"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "検索",
"theme.SearchPage.inputPlaceholder": "ここに検索するキーワードを入力してください",
"theme.SearchPage.noResultsText": "検索結果が見つかりませんでした",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "ブログ記事一覧のナビゲーション",
"theme.blog.paginator.newerEntries": "新しい記事",
"theme.blog.paginator.olderEntries": "過去の記事",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "サイドバーを隠す",
"theme.docs.sidebar.expandButtonAriaLabel": "サイドバーを開く",
"theme.docs.sidebar.expandButtonTitle": "サイドバーを開く",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "メニューを閉じる",
"theme.docs.sidebar.responsiveOpenButtonLabel": "メニューを開く",
"theme.docs.versions.latestVersionLinkLabel": "最新バージョン",
"theme.docs.versions.latestVersionSuggestionLabel": "最新のドキュメントは{latestVersionLink} ({versionLabel}) を見てください。",
"theme.docs.versions.unmaintainedVersionLabel": "これは{siteTitle} {versionLabel}のドキュメントで現在はアクティブにメンテナンスされていません。",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": "{date}に",
"theme.lastUpdated.byUser": "{user}が",
"theme.lastUpdated.lastUpdatedAtBy": "{atDate}{byUser}最終更新",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "タグ:",
"theme.tags.tagsPageLink": "全てのタグを見る",
"theme.tags.tagsPageTitle": "タグ"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "검색",
"theme.SearchPage.inputPlaceholder": "여기에 검색할 키워드를 입력하세요.",
"theme.SearchPage.noResultsText": "검색 결과가 없습니다.",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "블로그 게시물 목록 탐색",
"theme.blog.paginator.newerEntries": "이전 페이지",
"theme.blog.paginator.olderEntries": "다음 페이지",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "사이드바 숨기기",
"theme.docs.sidebar.expandButtonAriaLabel": "사이드바 열기",
"theme.docs.sidebar.expandButtonTitle": "사이드바 열기",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "메뉴 닫기",
"theme.docs.sidebar.responsiveOpenButtonLabel": "메뉴 열기",
"theme.docs.versions.latestVersionLinkLabel": "최신 버전",
"theme.docs.versions.latestVersionSuggestionLabel": "최신 문서는 {latestVersionLink} ({versionLabel})을 확인하세요.",
"theme.docs.versions.unmaintainedVersionLabel": "{siteTitle} {versionLabel} 문서는 업데이트되지 않습니다.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " {date}에",
"theme.lastUpdated.byUser": " {user}가",
"theme.lastUpdated.lastUpdatedAtBy": "{atDate}{byUser} 마지막으로 업데이트했습니다.",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "태그:",
"theme.tags.tagsPageLink": "모든 태그 보기",
"theme.tags.tagsPageTitle": "태그"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Szukaj",
"theme.SearchPage.inputPlaceholder": "wpisz szukaną frazę tutaj…",
"theme.SearchPage.noResultsText": "Nie znaleziono żadnych wyników",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Nawigacja na stronie listy wpisów na blogu",
"theme.blog.paginator.newerEntries": "Nowsze wpisy",
"theme.blog.paginator.olderEntries": "Starsze wpisy",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Zwiń boczny panel",
"theme.docs.sidebar.expandButtonAriaLabel": "Rozszerz boczny panel",
"theme.docs.sidebar.expandButtonTitle": "Rozszerz boczny panel",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Zamknij menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Otwórz menu",
"theme.docs.versions.latestVersionLinkLabel": "bieżącej wersji",
"theme.docs.versions.latestVersionSuggestionLabel": "Aby zobaczyć bieżącą dokumentację, przejdź do wersji {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ta dokumentacja dotyczy {siteTitle} w wersji {versionLabel} i nie jest już aktywnie aktualizowana.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " dnia {date}",
"theme.lastUpdated.byUser": " przez {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Ostatnia aktualizacja{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Tagi:",
"theme.tags.tagsPageLink": "Wyświetl wszystkie tagi",
"theme.tags.tagsPageTitle": "Tagi"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Buscar",
"theme.SearchPage.inputPlaceholder": "Digite sua busca aqui",
"theme.SearchPage.noResultsText": "Nenhum resultado foi encontrado",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Navegação da página de listagem do blog",
"theme.blog.paginator.newerEntries": "Conteúdo mais novo",
"theme.blog.paginator.olderEntries": "Conteúdo mais antigo",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Fechar painel lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir painel lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir painel lateral",
"theme.docs.sidebar.navAriaLabel": "Navegação do painel lateral",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Fechar menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Abrir menu",
"theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que não é mais mantida ativamente.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " em {date}",
"theme.lastUpdated.byUser": " por {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Última atualização {atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Marcadores:",
"theme.tags.tagsPageLink": "Ver todas os Marcadores",
"theme.tags.tagsPageTitle": "Marcadores"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Pesquisar",
"theme.SearchPage.inputPlaceholder": "Escreva aqui a sua pesquisa",
"theme.SearchPage.noResultsText": "Nenhum resultado encontrado",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Navegação da página de listagem do blog",
"theme.blog.paginator.newerEntries": "Publicações mais recentes",
"theme.blog.paginator.olderEntries": "Publicações mais antigas",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Fechar menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Abrir menu",
"theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que já não é mantida ativamente.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " a {date}",
"theme.lastUpdated.byUser": " por {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Última atualização{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Tags:",
"theme.tags.tagsPageLink": "Ver todas as Tags",
"theme.tags.tagsPageTitle": "Tags"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Поиск",
"theme.SearchPage.inputPlaceholder": "Введите фразу для поиска",
"theme.SearchPage.noResultsText": "По запросу ничего не найдено",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Навигация по странице списка блогов",
"theme.blog.paginator.newerEntries": "Следующие записи",
"theme.blog.paginator.olderEntries": "Предыдущие записи",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
"theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
"theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Закрыть меню",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Открыть меню",
"theme.docs.versions.latestVersionLinkLabel": "последней версии",
"theme.docs.versions.latestVersionSuggestionLabel": "Актуальная документация находится на странице {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Это документация {siteTitle} для версии {versionLabel}, которая уже не поддерживается.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " {date}",
"theme.lastUpdated.byUser": " от {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Последнее обновление{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Теги:",
"theme.tags.tagsPageLink": "Посмотреть все теги",
"theme.tags.tagsPageTitle": "Теги"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Ara",
"theme.SearchPage.inputPlaceholder": "Aramanızı buraya yazın",
"theme.SearchPage.noResultsText": "Hiçbir sonuç bulunamadı",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Blog gönderi sayfası navigasyonu",
"theme.blog.paginator.newerEntries": "Yeni Girdiler",
"theme.blog.paginator.olderEntries": "Eski Girdiler",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Kenar çubuğunu daralt",
"theme.docs.sidebar.expandButtonAriaLabel": "Kenar çubuğunu genişlet",
"theme.docs.sidebar.expandButtonTitle": "Kenar çubuğunu genişlet",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Menüyü kapat",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Menüyü aç",
"theme.docs.versions.latestVersionLinkLabel": "en son sürüm",
"theme.docs.versions.latestVersionSuggestionLabel": "Güncel belgeler için bkz. {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Bu, {siteTitle} {versionLabel} dokümantasyonudur ve bakımı sonlanmıştır.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " {date} tarihinde",
"theme.lastUpdated.byUser": " {user} tarafından",
"theme.lastUpdated.lastUpdatedAtBy": "En son{atDate}{byUser} güncellendi",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Etiketler:",
"theme.tags.tagsPageLink": "Tüm Etiketleri Görüntüle",
"theme.tags.tagsPageTitle": "Etiketler"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "Tìm kiếm",
"theme.SearchPage.inputPlaceholder": "Nhập từ khóa cần tìm vào đây",
"theme.SearchPage.noResultsText": "Không tìm thấy kết quả nào",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "Thanh điều hướng của trang danh sách bài viết",
"theme.blog.paginator.newerEntries": "Các bài mới hơn",
"theme.blog.paginator.olderEntries": "Các bài cũ hơn",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "Thu gọn thanh bên",
"theme.docs.sidebar.expandButtonAriaLabel": "Mở rộng thanh bên",
"theme.docs.sidebar.expandButtonTitle": "Mở rộng thanh bên",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Đóng menu",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Mở menu",
"theme.docs.versions.latestVersionLinkLabel": "phiên bản mới nhất",
"theme.docs.versions.latestVersionSuggestionLabel": "Để xem các cập nhật mới nhất, vui lòng xem phiên bản {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Đây là tài liệu của {siteTitle} {versionLabel}, hiện không còn được bảo trì.",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": " vào {date}",
"theme.lastUpdated.byUser": " bởi {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Cập nhật lần cuối{atDate}{byUser}",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "Thẻ:",
"theme.tags.tagsPageLink": "Xem tất cả Thẻ",
"theme.tags.tagsPageTitle": "Thẻ"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "搜索",
"theme.SearchPage.inputPlaceholder": "在此输入搜索字词",
"theme.SearchPage.noResultsText": "未找到任何结果",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "博文列表分页导航",
"theme.blog.paginator.newerEntries": "较新的博文",
"theme.blog.paginator.olderEntries": "较旧的博文",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "收起侧边栏",
"theme.docs.sidebar.expandButtonAriaLabel": "展开侧边栏",
"theme.docs.sidebar.expandButtonTitle": "展开侧边栏",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "关闭菜单",
"theme.docs.sidebar.responsiveOpenButtonLabel": "打开菜单",
"theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": "于 {date} ",
"theme.lastUpdated.byUser": "由 {user} ",
"theme.lastUpdated.lastUpdatedAtBy": "最后{byUser}{atDate}更新",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "标签:",
"theme.tags.tagsPageLink": "查看所有标签",
"theme.tags.tagsPageTitle": "标签"

View file

@ -20,6 +20,7 @@
"theme.SearchPage.inputLabel": "搜尋",
"theme.SearchPage.inputPlaceholder": "在此輸入搜尋字詞",
"theme.SearchPage.noResultsText": "未找到任何結果",
"theme.TOCCollapsible.toggleButtonLabel": "On this page",
"theme.blog.paginator.navAriaLabel": "部落格文章列表分頁導覽",
"theme.blog.paginator.newerEntries": "較新的文章",
"theme.blog.paginator.olderEntries": "較舊的文章",
@ -41,9 +42,6 @@
"theme.docs.sidebar.collapseButtonTitle": "收起側邊欄",
"theme.docs.sidebar.expandButtonAriaLabel": "展開側邊欄",
"theme.docs.sidebar.expandButtonTitle": "展開側邊欄",
"theme.docs.sidebar.navAriaLabel": "Sidebar navigation",
"theme.docs.sidebar.responsiveCloseButtonLabel": "關閉選單",
"theme.docs.sidebar.responsiveOpenButtonLabel": "打開選單",
"theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文件請參閱 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此為 {siteTitle} {versionLabel} 版的文件,現已不再積極維護。",
@ -51,6 +49,7 @@
"theme.lastUpdated.atDate": "於 {date} ",
"theme.lastUpdated.byUser": "由 {user} ",
"theme.lastUpdated.lastUpdatedAtBy": "最後{byUser}{atDate}更新",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
"theme.tags.tagsListLabel": "標籤:",
"theme.tags.tagsPageLink": "查看所有標籤",
"theme.tags.tagsPageTitle": "標籤"

View file

@ -6,18 +6,21 @@
*/
import React from 'react';
import clsx from 'clsx';
import {useActivePlugin, useVersions} from '@theme/hooks/useDocs';
import useWindowSize from '@theme/hooks/useWindowSize';
import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import Seo from '@theme/Seo';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem';
import TOC from '@theme/TOC';
import TOCCollapsible from '@theme/TOCCollapsible';
import EditThisPage from '@theme/EditThisPage';
import {MainHeading} from '@theme/Heading';
import clsx from 'clsx';
import styles from './styles.module.css';
import {useActivePlugin, useVersions} from '@theme/hooks/useDocs';
function DocItem(props: Props): JSX.Element {
const {content: DocContent, versionMetadata} = props;
@ -51,6 +54,18 @@ function DocItem(props: Props): JSX.Element {
const shouldAddTitle =
!hideTitle && typeof DocContent.contentTitle === 'undefined';
const windowSize = useWindowSize();
const renderTocMobile =
!hideTableOfContents &&
DocContent.toc &&
(windowSize === 'mobile' || windowSize === 'ssr');
const renderTocDesktop =
!hideTableOfContents &&
DocContent.toc &&
(windowSize === 'desktop' || windowSize === 'ssr');
return (
<>
<Seo {...{title, description, keywords, image}} />
@ -69,6 +84,13 @@ function DocItem(props: Props): JSX.Element {
</span>
)}
{renderTocMobile && (
<TOCCollapsible
toc={DocContent.toc}
className={styles.tocMobile}
/>
)}
<div className="markdown">
{/*
Title can be declared inside md content or declared through frontmatter and added manually
@ -102,7 +124,7 @@ function DocItem(props: Props): JSX.Element {
<DocPaginator metadata={metadata} />
</div>
</div>
{!hideTableOfContents && DocContent.toc && (
{renderTocDesktop && (
<div className="col col--3">
<TOC toc={DocContent.toc} />
</div>

View file

@ -23,4 +23,9 @@
.lastUpdated {
text-align: right;
}
/* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */
.tocMobile {
display: none;
}
}

View file

@ -31,28 +31,6 @@ type DocPageContentProps = {
readonly children: ReactNode;
};
function getSidebar({versionMetadata, currentDocRoute}) {
function addTrailingSlash(str: string): string {
return str.endsWith('/') ? str : `${str}/`;
}
function removeTrailingSlash(str: string): string {
return str.endsWith('/') ? str.slice(0, -1) : str;
}
const {permalinkToSidebar, docsSidebars} = versionMetadata;
// With/without trailingSlash, we should always be able to get the appropriate sidebar
// note: docs plugin permalinks currently never have trailing slashes
// trailingSlash is handled globally at the framework level, not plugin level
const sidebarName =
permalinkToSidebar[currentDocRoute.path] ||
permalinkToSidebar[addTrailingSlash(currentDocRoute.path)] ||
permalinkToSidebar[removeTrailingSlash(currentDocRoute.path)];
const sidebar = docsSidebars[sidebarName];
return {sidebar, sidebarName};
}
function DocPageContent({
currentDocRoute,
versionMetadata,
@ -60,7 +38,11 @@ function DocPageContent({
}: DocPageContentProps): JSX.Element {
const {siteConfig, isClient} = useDocusaurusContext();
const {pluginId, version} = versionMetadata;
const {sidebarName, sidebar} = getSidebar({versionMetadata, currentDocRoute});
const sidebarName = currentDocRoute.sidebar;
const sidebar = sidebarName
? versionMetadata.docsSidebars[sidebarName]
: undefined;
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false);
const [hiddenSidebar, setHiddenSidebar] = useState(false);
@ -106,9 +88,7 @@ function DocPageContent({
}
sidebar={sidebar}
path={currentDocRoute.path}
sidebarCollapsible={
siteConfig.themeConfig?.sidebarCollapsible ?? true
}
sidebarCollapsible={siteConfig.themeConfig.sidebarCollapsible}
onCollapse={toggleSidebar}
isHidden={hiddenSidebar}
/>

View file

@ -20,6 +20,10 @@
width: 100%;
}
.docSidebarContainer {
display: none;
}
@media (min-width: 997px) {
.docMainContainer {
flex-grow: 1;
@ -31,6 +35,7 @@
}
.docSidebarContainer {
display: block;
width: var(--doc-sidebar-width);
margin-top: calc(-1 * var(--ifm-navbar-height));
border-right: 1px solid var(--ifm-toc-border-color);

View file

@ -5,175 +5,24 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {useState, useCallback, useEffect, memo} from 'react';
import React, {useState} from 'react';
import clsx from 'clsx';
import {
useThemeConfig,
isSamePath,
usePrevious,
useAnnouncementBar,
useCollapsible,
MobileSecondaryMenuFiller,
MobileSecondaryMenuComponent,
} from '@docusaurus/theme-common';
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
import useWindowSize, {windowSizes} from '@theme/hooks/useWindowSize';
import useWindowSize from '@theme/hooks/useWindowSize';
import useScrollPosition from '@theme/hooks/useScrollPosition';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
import type {Props} from '@theme/DocSidebar';
import Logo from '@theme/Logo';
import IconArrow from '@theme/IconArrow';
import IconMenu from '@theme/IconMenu';
import IconExternalLink from '@theme/IconExternalLink';
import {translate} from '@docusaurus/Translate';
import {DocSidebarItems} from '@theme/DocSidebarItem';
import type {Props} from '@theme/DocSidebar';
import styles from './styles.module.css';
const MOBILE_TOGGLE_SIZE = 24;
const isActiveSidebarItem = (item, activePath) => {
if (item.type === 'link') {
return isSamePath(item.href, activePath);
}
if (item.type === 'category') {
return item.items.some((subItem) =>
isActiveSidebarItem(subItem, activePath),
);
}
return false;
};
// Optimize sidebar at each "level"
// TODO this item should probably not receive the "activePath" props
// TODO this triggers whole sidebar re-renders on navigation
const DocSidebarItems = memo(function DocSidebarItems({
items,
...props
}: any): JSX.Element {
return items.map((item, index) => (
<DocSidebarItem
key={index} // sidebar is static, the index does not change
item={item}
{...props}
/>
));
});
function DocSidebarItem(props): JSX.Element | null {
switch (props.item.type) {
case 'category':
// Never render empty categories
if (props.item.items.length === 0) {
return null;
}
return <DocSidebarItemCategory {...props} />;
case 'link':
default:
return <DocSidebarItemLink {...props} />;
}
}
function DocSidebarItemCategory({
item,
onItemClick,
collapsible,
activePath,
...props
}) {
const {items, label} = item;
const isActive = isActiveSidebarItem(item, activePath);
const wasActive = usePrevious(isActive);
const {
collapsed,
setCollapsed,
getToggleProps,
getCollapsibleProps,
} = useCollapsible({
// active categories are always initialized as expanded
// the default (item.collapsed) is only used for non-active categories
initialState: () => {
if (!collapsible) {
return false;
}
return isActive ? false : item.collapsed;
},
});
// If we navigate to a category, it should automatically expand itself
useEffect(() => {
const justBecameActive = isActive && !wasActive;
if (justBecameActive && collapsed) {
setCollapsed(false);
}
}, [isActive, wasActive, collapsed]);
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--active': collapsible && isActive,
[styles.menuLinkText]: !collapsible,
})}
href={collapsible ? '#' : undefined}
{...getToggleProps()}
{...props}>
{label}
</a>
<ul className="menu__list" {...getCollapsibleProps()}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? '-1' : '0'}
onItemClick={onItemClick}
collapsible={collapsible}
activePath={activePath}
/>
</ul>
</li>
);
}
function DocSidebarItemLink({
item,
onItemClick,
activePath,
collapsible: _collapsible,
...props
}) {
const {href, label} = item;
const isActive = isActiveSidebarItem(item, activePath);
return (
<li className="menu__list-item" key={label}>
<Link
className={clsx('menu__link', {
'menu__link--active': isActive,
})}
to={href}
{...(isInternalUrl(href) && {
isNavLink: true,
exact: true,
onClick: onItemClick,
})}
{...props}>
{isInternalUrl(href) ? (
label
) : (
<span>
{label}
<IconExternalLink />
</span>
)}
</Link>
</li>
);
}
function useShowAnnouncementBar() {
const {isClosed} = useAnnouncementBar();
const [showAnnouncementBar, setShowAnnouncementBar] = useState(!isClosed);
@ -185,36 +34,6 @@ function useShowAnnouncementBar() {
return showAnnouncementBar;
}
function useResponsiveSidebar() {
const [showResponsiveSidebar, setShowResponsiveSidebar] = useState(false);
useLockBodyScroll(showResponsiveSidebar);
const windowSize = useWindowSize();
useEffect(() => {
if (windowSize === windowSizes.desktop) {
setShowResponsiveSidebar(false);
}
}, [windowSize]);
const closeResponsiveSidebar = useCallback(
(e) => {
e.target.blur();
setShowResponsiveSidebar(false);
},
[setShowResponsiveSidebar],
);
const toggleResponsiveSidebar = useCallback(() => {
setShowResponsiveSidebar((value) => !value);
}, [setShowResponsiveSidebar]);
return {
showResponsiveSidebar,
closeResponsiveSidebar,
toggleResponsiveSidebar,
};
}
function HideableSidebarButton({onClick}) {
return (
<button
@ -239,51 +58,13 @@ function HideableSidebarButton({onClick}) {
);
}
function ResponsiveSidebarButton({responsiveSidebarOpened, onClick}) {
return (
<button
aria-label={
responsiveSidebarOpened
? translate({
id: 'theme.docs.sidebar.responsiveCloseButtonLabel',
message: 'Close menu',
description:
'The ARIA label for close button of mobile doc sidebar',
})
: translate({
id: 'theme.docs.sidebar.responsiveOpenButtonLabel',
message: 'Open menu',
description:
'The ARIA label for open button of mobile doc sidebar',
})
}
aria-haspopup="true"
className="button button--secondary button--sm menu__button"
type="button"
onClick={onClick}>
{responsiveSidebarOpened ? (
<span
className={clsx(styles.sidebarMenuIcon, styles.sidebarMenuCloseIcon)}>
&times;
</span>
) : (
<IconMenu
className={styles.sidebarMenuIcon}
height={MOBILE_TOGGLE_SIZE}
width={MOBILE_TOGGLE_SIZE}
/>
)}
</button>
);
}
function DocSidebar({
function DocSidebarDesktop({
path,
sidebar,
sidebarCollapsible = true,
sidebarCollapsible,
onCollapse,
isHidden,
}: Props): JSX.Element | null {
}: Props) {
const showAnnouncementBar = useShowAnnouncementBar();
const {
navbar: {hideOnScroll},
@ -291,12 +72,6 @@ function DocSidebar({
} = useThemeConfig();
const {isClosed: isAnnouncementBarClosed} = useAnnouncementBar();
const {
showResponsiveSidebar,
closeResponsiveSidebar,
toggleResponsiveSidebar,
} = useResponsiveSidebar();
return (
<div
className={clsx(styles.sidebar, {
@ -305,31 +80,13 @@ function DocSidebar({
})}>
{hideOnScroll && <Logo tabIndex={-1} className={styles.sidebarLogo} />}
<nav
className={clsx(
'menu',
'menu--responsive',
'thin-scrollbar',
styles.menu,
{
'menu--show': showResponsiveSidebar,
[styles.menuWithAnnouncementBar]:
!isAnnouncementBarClosed && showAnnouncementBar,
},
)}
aria-label={translate({
id: 'theme.docs.sidebar.navAriaLabel',
message: 'Sidebar navigation',
description: 'The ARIA label for documentation menu',
className={clsx('menu thin-scrollbar', styles.menu, {
[styles.menuWithAnnouncementBar]:
!isAnnouncementBarClosed && showAnnouncementBar,
})}>
<ResponsiveSidebarButton
responsiveSidebarOpened={showResponsiveSidebar}
onClick={toggleResponsiveSidebar}
/>
<ul className="menu__list">
<DocSidebarItems
items={sidebar}
onItemClick={closeResponsiveSidebar}
collapsible={sidebarCollapsible}
activePath={path}
/>
@ -340,4 +97,50 @@ function DocSidebar({
);
}
export default DocSidebar;
const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
toggleSidebar,
sidebar,
sidebarCollapsible,
path,
}) => {
return (
<ul className="menu__list">
<DocSidebarItems
items={sidebar}
collapsible={sidebarCollapsible}
activePath={path}
onItemClick={() => toggleSidebar()}
/>
</ul>
);
};
function DocSidebarMobile(props: Props) {
return (
<MobileSecondaryMenuFiller
component={DocSidebarMobileSecondaryMenu}
props={props}
/>
);
}
const DocSidebarDesktopMemo = React.memo(DocSidebarDesktop);
const DocSidebarMobileMemo = React.memo(DocSidebarMobile);
export default function DocSidebar(props: Props): JSX.Element {
const windowSize = useWindowSize();
// Desktop sidebar visible on hydration: need SSR rendering
const shouldRenderSidebarDesktop =
windowSize === 'desktop' || windowSize === 'ssr';
// Mobile sidebar not visible on hydration: can avoid SSR rendering
const shouldRenderSidebarMobile = windowSize === 'mobile';
return (
<>
{shouldRenderSidebarDesktop && <DocSidebarDesktopMemo {...props} />}
{shouldRenderSidebarMobile && <DocSidebarMobileMemo {...props} />}
</>
);
}

View file

@ -53,14 +53,6 @@
padding: 0.5rem;
}
.menuLinkText {
cursor: initial;
}
.menuLinkText:hover {
background: none;
}
.menuWithAnnouncementBar {
margin-bottom: var(--docusaurus-announcement-bar-height);
}

View file

@ -0,0 +1,183 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {useEffect, memo} from 'react';
import clsx from 'clsx';
import {
isSamePath,
usePrevious,
Collapsible,
useCollapsible,
} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
import IconExternalLink from '@theme/IconExternalLink';
import type {Props, DocSidebarItemsProps} from '@theme/DocSidebarItem';
import type {
PropSidebarItemCategory,
PropSidebarItemLink,
} from '@docusaurus/plugin-content-docs-types';
import styles from './styles.module.css';
const isActiveSidebarItem = (item: Props['item'], activePath: string) => {
if (item.type === 'link') {
return isSamePath(item.href, activePath);
}
if (item.type === 'category') {
return item.items.some((subItem) =>
isActiveSidebarItem(subItem, activePath),
);
}
return false;
};
// Optimize sidebar at each "level"
// TODO this item should probably not receive the "activePath" props
// TODO this triggers whole sidebar re-renders on navigation
export const DocSidebarItems = memo(function DocSidebarItems({
items,
...props
}: DocSidebarItemsProps): JSX.Element {
return (
<>
{items.map((item, index) => (
<DocSidebarItem
key={index} // sidebar is static, the index does not change
item={item}
{...props}
/>
))}
</>
);
});
export default function DocSidebarItem({
item,
...props
}: Props): JSX.Element | null {
switch (item.type) {
case 'category':
if (item.items.length === 0) {
return null;
}
return <DocSidebarItemCategory item={item} {...props} />;
case 'link':
default:
return <DocSidebarItemLink item={item} {...props} />;
}
}
// If we navigate to a category and it becomes active, it should automatically expand itself
function useAutoExpandActiveCategory({
isActive,
collapsed,
setCollapsed,
}: {
isActive: boolean;
collapsed: boolean;
setCollapsed: (b: boolean) => void;
}) {
const wasActive = usePrevious(isActive);
useEffect(() => {
const justBecameActive = isActive && !wasActive;
if (justBecameActive && collapsed) {
setCollapsed(false);
}
}, [isActive, wasActive, collapsed]);
}
function DocSidebarItemCategory({
item,
onItemClick,
collapsible = true,
activePath,
...props
}: Props & {item: PropSidebarItemCategory}) {
const {items, label} = item;
const isActive = isActiveSidebarItem(item, activePath);
const {collapsed, setCollapsed, toggleCollapsed} = useCollapsible({
// active categories are always initialized as expanded
// the default (item.collapsed) is only used for non-active categories
initialState: () => {
if (!collapsible) {
return false;
}
return isActive ? false : item.collapsed ?? true;
},
});
useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--active': collapsible && isActive,
[styles.menuLinkText]: !collapsible,
})}
onClick={collapsible ? toggleCollapsed : undefined}
href={collapsible ? '#' : undefined}
{...props}>
{label}
</a>
<Collapsible as="ul" className="menu__list" collapsed={collapsed}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
collapsible={collapsible}
activePath={activePath}
/>
</Collapsible>
</li>
);
}
function DocSidebarItemLink({
item,
onItemClick,
activePath,
collapsible: _collapsible,
...props
}: Props & {item: PropSidebarItemLink}) {
const {href, label} = item;
const isActive = isActiveSidebarItem(item, activePath);
return (
<li className="menu__list-item" key={label}>
<Link
className={clsx('menu__link', {
'menu__link--active': isActive,
})}
to={href}
{...(isInternalUrl(href) && {
isNavLink: true,
exact: true,
onClick: onItemClick,
})}
{...props}>
{isInternalUrl(href) ? (
label
) : (
<span>
{label}
<IconExternalLink />
</span>
)}
</Link>
</li>
);
}

View file

@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
@media (min-width: 997px) {
.menuLinkText {
cursor: initial;
}
.menuLinkText:hover {
background: none;
}
}

View file

@ -11,6 +11,7 @@ import UserPreferencesProvider from '@theme/UserPreferencesProvider';
import {
AnnouncementBarProvider,
DocsPreferredVersionContextProvider,
MobileSecondaryMenuProvider,
} from '@docusaurus/theme-common';
import type {Props} from '@theme/LayoutProviders';
@ -20,7 +21,9 @@ export default function LayoutProviders({children}: Props): JSX.Element {
<AnnouncementBarProvider>
<UserPreferencesProvider>
<DocsPreferredVersionContextProvider>
{children}
<MobileSecondaryMenuProvider>
{children}
</MobileSecondaryMenuProvider>
</DocsPreferredVersionContextProvider>
</UserPreferencesProvider>
</AnnouncementBarProvider>

View file

@ -7,14 +7,17 @@
import React, {useCallback, useState, useEffect} from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import SearchBar from '@theme/SearchBar';
import Toggle from '@theme/Toggle';
import useThemeContext from '@theme/hooks/useThemeContext';
import {useThemeConfig} from '@docusaurus/theme-common';
import {
useThemeConfig,
useMobileSecondaryMenuRenderer,
} from '@docusaurus/theme-common';
import useHideableNavbar from '@theme/hooks/useHideableNavbar';
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
import useWindowSize, {windowSizes} from '@theme/hooks/useWindowSize';
import useWindowSize from '@theme/hooks/useWindowSize';
import NavbarItem from '@theme/NavbarItem';
import Logo from '@theme/Logo';
import IconMenu from '@theme/IconMenu';
@ -39,37 +42,120 @@ function splitNavItemsByPosition(items) {
};
}
function Navbar(): JSX.Element {
function NavbarMobileSidebar({
sidebarShown,
toggleSidebar,
}: {
sidebarShown: boolean;
toggleSidebar: () => void;
}) {
useLockBodyScroll(sidebarShown);
const {
navbar: {items, hideOnScroll, style},
navbar: {items},
colorMode: {disableSwitch: disableColorModeSwitch},
} = useThemeConfig();
const [sidebarShown, setSidebarShown] = useState(false);
const {isDarkTheme, setLightTheme, setDarkTheme} = useThemeContext();
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
useLockBodyScroll(sidebarShown);
const showSidebar = useCallback(() => {
setSidebarShown(true);
}, [setSidebarShown]);
const hideSidebar = useCallback(() => {
setSidebarShown(false);
}, [setSidebarShown]);
const onToggleChange = useCallback(
(e) => (e.target.checked ? setDarkTheme() : setLightTheme()),
[setLightTheme, setDarkTheme],
);
const mobileSecondaryMenuContent = useMobileSecondaryMenuRenderer()?.({
toggleSidebar,
});
const hasMobileSecondaryMenu = !!mobileSecondaryMenuContent;
const [mainMenuShown, setMainMenuShown] = useState(true);
// On sidebar close, reset the sidebar to secondary menu (if any)
useEffect(() => {
if (!hasMobileSecondaryMenu) {
setMainMenuShown(true);
return;
}
if (!sidebarShown) {
setMainMenuShown(false);
}
}, [sidebarShown, hasMobileSecondaryMenu]);
return (
<div className="navbar-sidebar">
<div className="navbar-sidebar__brand">
<Logo
className="navbar__brand"
imageClassName="navbar__logo"
titleClassName="navbar__title"
/>
{!disableColorModeSwitch && sidebarShown && (
<Toggle checked={isDarkTheme} onChange={onToggleChange} />
)}
</div>
<div
className={clsx('navbar-sidebar__items', styles.menuWrapper, {
[styles.menuWrapperDocShown]: !mainMenuShown,
})}>
<div className="menu">
<ul className="menu__list">
{items.map((item, i) => (
<NavbarItem
mobile
{...(item as any)} // TODO fix typing
onClick={toggleSidebar}
key={i}
/>
))}
</ul>
</div>
<div className={styles.docSidebarSecondaryMenu}>
<button
type="button"
className={clsx('clean-btn', styles.backButton)}
onClick={() => setMainMenuShown(true)}>
<Translate
id="theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel"
description="The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)">
Back to main menu
</Translate>
</button>
{mobileSecondaryMenuContent}
</div>
</div>
</div>
);
}
function Navbar(): JSX.Element {
const {
navbar: {items, hideOnScroll, style},
colorMode: {disableSwitch: disableColorModeSwitch},
} = useThemeConfig();
const windowSize = useWindowSize();
// Mobile sidebar not visible on hydration: can avoid SSR rendering
const shouldRenderSidebarMobile = windowSize === 'mobile'; // || windowSize === 'ssr';
const [sidebarShown, setSidebarShown] = useState(false);
const toggleSidebar = useCallback(() => {
setSidebarShown(!sidebarShown);
}, [sidebarShown]);
useEffect(() => {
if (windowSize === windowSizes.desktop) {
if (windowSize === 'desktop') {
setSidebarShown(false);
}
}, [windowSize]);
const {isDarkTheme, setLightTheme, setDarkTheme} = useThemeContext();
const onToggleChange = useCallback(
(e) => (e.target.checked ? setDarkTheme() : setLightTheme()),
[setLightTheme, setDarkTheme],
);
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
const hasSearchNavbarItem = items.some((item) => item.type === 'search');
const {leftItems, rightItems} = splitNavItemsByPosition(items);
@ -85,14 +171,14 @@ function Navbar(): JSX.Element {
})}>
<div className="navbar__inner">
<div className="navbar__items">
{items != null && items.length !== 0 && (
{items?.length > 0 && (
<button
aria-label="Navigation bar toggle"
className="navbar__toggle clean-btn"
type="button"
tabIndex={0}
onClick={showSidebar}
onKeyDown={showSidebar}>
onClick={toggleSidebar}
onKeyDown={toggleSidebar}>
<IconMenu />
</button>
)}
@ -119,38 +205,19 @@ function Navbar(): JSX.Element {
{!hasSearchNavbarItem && <SearchBar />}
</div>
</div>
<div
role="presentation"
className="navbar-sidebar__backdrop"
onClick={hideSidebar}
onClick={toggleSidebar}
/>
<div className="navbar-sidebar">
<div className="navbar-sidebar__brand">
<Logo
className="navbar__brand"
imageClassName="navbar__logo"
titleClassName="navbar__title"
onClick={hideSidebar}
/>
{!disableColorModeSwitch && sidebarShown && (
<Toggle checked={isDarkTheme} onChange={onToggleChange} />
)}
</div>
<div className="navbar-sidebar__items">
<div className="menu">
<ul className="menu__list">
{items.map((item, i) => (
<NavbarItem
mobile
{...(item as any)} // TODO fix typing
onClick={hideSidebar}
key={i}
/>
))}
</ul>
</div>
</div>
</div>
{shouldRenderSidebarMobile && (
<NavbarMobileSidebar
sidebarShown={sidebarShown}
toggleSidebar={toggleSidebar}
/>
)}
</nav>
);
}

View file

@ -18,3 +18,47 @@
.navbarHidden {
transform: translate3d(0, calc(-100% - 2px), 0);
}
:global(.navbar-sidebar__items) {
padding: 0;
}
:global(.navbar-sidebar) {
overflow-x: hidden;
}
.menuWrapper {
display: flex;
width: calc(var(--ifm-navbar-sidebar-width));
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateZ(0) !important;
}
.menuWrapperDocShown {
transform: translate3d(
calc((var(--ifm-navbar-sidebar-width)) * -1),
0,
0
) !important;
}
.menuWrapper > div {
flex-shrink: 0;
width: calc(var(--ifm-navbar-sidebar-width));
font-weight: var(--ifm-font-weight-semibold);
padding: 0.5rem;
}
.backButton {
width: calc(100% + 1rem);
margin: 0 0 0.7rem -0.5rem;
text-align: left;
font-size: 15px;
padding: 0.6rem 1.5rem;
background: var(--ifm-menu-color-background-active);
font-weight: var(--ifm-button-font-weight);
}
.docSidebarSecondaryMenu {
padding-top: 0 !important;
}

View file

@ -8,22 +8,15 @@
import React from 'react';
import clsx from 'clsx';
import useTOCHighlight from '@theme/hooks/useTOCHighlight';
import type {TOCProps} from '@theme/TOC';
import type {TOCProps, TOCHeadingsProps} from '@theme/TOC';
import styles from './styles.module.css';
import {TOCItem} from '@docusaurus/types';
const LINK_CLASS_NAME = 'table-of-contents__link';
const ACTIVE_LINK_CLASS_NAME = 'table-of-contents__link--active';
const TOP_OFFSET = 100;
/* eslint-disable jsx-a11y/control-has-associated-label */
function Headings({
toc,
isChild,
}: {
toc: readonly TOCItem[];
isChild?: boolean;
}) {
export function TOCHeadings({toc, isChild}: TOCHeadingsProps) {
if (!toc.length) {
return null;
}
@ -41,7 +34,7 @@ function Headings({
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{__html: heading.value}}
/>
<Headings isChild toc={heading.children} />
<TOCHeadings isChild toc={heading.children} />
</li>
))}
</ul>
@ -52,7 +45,7 @@ function TOC({toc}: TOCProps): JSX.Element {
useTOCHighlight(LINK_CLASS_NAME, ACTIVE_LINK_CLASS_NAME, TOP_OFFSET);
return (
<div className={clsx(styles.tableOfContents, 'thin-scrollbar')}>
<Headings toc={toc} />
<TOCHeadings toc={toc} />
</div>
);
}

View file

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import {useCollapsible, Collapsible} from '@docusaurus/theme-common';
import styles from './styles.module.css';
import {TOCHeadings} from '@theme/TOC';
import type {TOCCollapsibleProps} from '@theme/TOCCollapsible';
export default function TOCCollapsible({toc, className}: TOCCollapsibleProps) {
const {collapsed, toggleCollapsed} = useCollapsible({
initialState: true,
});
return (
<div
className={clsx(
'margin-vert--md',
styles.tocCollapsible,
{
[styles.tocCollapsibleExpanded]: !collapsed,
},
className,
)}>
<button
type="button"
className={clsx('clean-btn', styles.tocCollapsibleButton)}
onClick={toggleCollapsed}>
<Translate
id="theme.TOCCollapsible.toggleButtonLabel"
description="The label used by the button on the collapsible TOC component">
On this page
</Translate>
</button>
<Collapsible
className={styles.tocCollapsibleContent}
collapsed={collapsed}>
<TOCHeadings toc={toc} />
</Collapsible>
</div>
);
}

View file

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.tocCollapsible {
background-color: var(--ifm-menu-color-background-active);
border-radius: var(--ifm-global-radius);
}
.tocCollapsibleButton {
font-size: inherit;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.8rem;
width: 100%;
}
.tocCollapsibleButton:after {
content: '';
background: var(--ifm-menu-link-sublist-icon) 50% 50% / 2rem 2rem no-repeat;
filter: var(--ifm-menu-link-sublist-icon-filter);
height: 1.25rem;
width: 1.25rem;
transform: rotate(180deg);
transition: transform var(--ifm-transition-fast);
}
.tocCollapsibleContent > ul {
border-left: none;
border-top: 1px solid var(--ifm-color-emphasis-300);
padding: 0.2rem 0;
font-size: 15px;
}
.tocCollapsibleContent ul li {
margin: 0.4rem 0.8rem;
}
.tocCollapsibleContent a {
display: block;
}
.tocCollapsibleExpanded .tocCollapsibleButton:after {
transform: none;
}

View file

@ -10,42 +10,65 @@ import {useEffect, useState} from 'react';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import type {WindowSize} from '@theme/hooks/useWindowSize';
const desktopThresholdWidth = 996;
const windowSizes = {
desktop: 'desktop',
mobile: 'mobile',
// This "ssr" value is very important to handle hydration FOUC / layout shifts
// You have to handle server-rendering explicitly on the call-site
// On the server, you may need to render BOTH the mobile/desktop elements (and hide one of them with mediaquery)
// We don't return "undefined" on purpose, to make it more explicit
ssr: 'ssr',
} as const;
function useWindowSize(): WindowSize | undefined {
const isClient = ExecutionEnvironment.canUseDOM;
const DesktopThresholdWidth = 996;
function getSize() {
if (!isClient) {
return undefined;
}
return window.innerWidth > desktopThresholdWidth
? windowSizes.desktop
: windowSizes.mobile;
function getWindowSize() {
if (!ExecutionEnvironment.canUseDOM) {
return windowSizes.ssr;
}
return window.innerWidth > DesktopThresholdWidth
? windowSizes.desktop
: windowSizes.mobile;
}
const [windowSize, setWindowSize] = useState(getSize);
// Simulate the SSR window size in dev, so that potential hydration FOUC/layout shift problems can be seen in dev too!
const DevSimulateSSR = process.env.NODE_ENV === 'development' && true;
// This hook returns an enum value on purpose!
// We don't want it to return the actual width value, for resize perf reasons
// We only want to re-render once a breakpoint is crossed
function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>(() => {
if (DevSimulateSSR) {
return 'ssr';
}
return getWindowSize();
});
useEffect(() => {
if (!isClient) {
if (!ExecutionEnvironment.canUseDOM) {
return undefined;
}
function handleResize() {
setWindowSize(getSize());
function updateWindowSize() {
setWindowSize(getWindowSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
// @ts-expect-error: annoying TS setTimeout typing...
const timeout: number | undefined = DevSimulateSSR
? setTimeout(updateWindowSize, 1000)
: undefined;
window.addEventListener('resize', updateWindowSize);
return () => {
window.removeEventListener('resize', updateWindowSize);
clearTimeout(timeout);
};
}, []);
return windowSize;
}
export {windowSizes};
export default useWindowSize;

View file

@ -88,6 +88,28 @@ declare module '@theme/DocSidebar' {
export default DocSidebar;
}
declare module '@theme/DocSidebarItem' {
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs-types';
type DocSidebarPropsBase = {
readonly activePath: string;
readonly collapsible?: boolean;
readonly onItemClick?: () => void;
readonly tabIndex?: number;
};
export type Props = DocSidebarPropsBase & {
readonly item: PropSidebarItem;
};
const DocSidebarItem: (props: Props) => JSX.Element;
export default DocSidebarItem;
export type DocSidebarItemsProps = DocSidebarPropsBase & {
readonly items: readonly PropSidebarItem[];
};
export const DocSidebarItems: (props: DocSidebarItemsProps) => JSX.Element;
}
declare module '@theme/DocVersionSuggestions' {
const DocVersionSuggestions: () => JSX.Element;
export default DocVersionSuggestions;
@ -213,11 +235,12 @@ declare module '@theme/hooks/useWindowSize' {
export const windowSizes: {
desktop: 'desktop';
mobile: 'mobile';
ssr: 'ssr';
};
export type WindowSize = keyof typeof windowSizes;
export default function useWindowSize(): WindowSize | undefined;
export default function useWindowSize(): WindowSize;
}
declare module '@theme/hooks/useKeyboardNavigation' {
@ -476,6 +499,13 @@ declare module '@theme/TOC' {
readonly toc: readonly TOCItem[];
};
export type TOCHeadingsProps = {
readonly toc: readonly TOCItem[];
readonly isChild?: boolean;
};
export const TOCHeadings: (props: HeadingsProps) => JSX.Element;
const TOC: (props: TOCProps) => JSX.Element;
export default TOC;
}
@ -491,6 +521,18 @@ declare module '@theme/TOCInline' {
export default TOCInline;
}
declare module '@theme/TOCCollapsible' {
import type {TOCItem} from '@docusaurus/types';
export type TOCCollapsibleProps = {
readonly className?: string;
readonly toc: readonly TOCItem[];
};
const TOCCollapsible: (props: TOCCollapsibleProps) => JSX.Element;
export default TOCCollapsible;
}
declare module '@theme/Toggle' {
import type {SyntheticEvent} from 'react';

View file

@ -40,6 +40,7 @@ const DEFAULT_CONFIG = {
items: [],
},
hideableSidebar: false,
sidebarCollapsible: true,
};
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
@ -309,6 +310,7 @@ const ThemeConfigSchema = Joi.object({
.default(DEFAULT_CONFIG.prism)
.unknown(),
hideableSidebar: Joi.bool().default(DEFAULT_CONFIG.hideableSidebar),
sidebarCollapsible: Joi.bool().default(DEFAULT_CONFIG.sidebarCollapsible),
});
exports.ThemeConfigSchema = ThemeConfigSchema;

View file

@ -149,10 +149,12 @@ ${logKeys(unknownMessages)}`),
const newBaseMessagesDescriptions = Object.entries(newBaseMessages).reduce(
(acc, [key]) => {
const codeTranslation = codeExtractedTranslations[key];
return {
...acc,
[`${key}${DescriptionSuffix}`]: codeExtractedTranslations[key]
.description,
[`${key}${DescriptionSuffix}`]: codeTranslation
? codeTranslation.description
: undefined,
};
},
{},

View file

@ -37,12 +37,19 @@ export {useLocationChange} from './utils/useLocationChange';
export {usePrevious} from './utils/usePrevious';
export {useCollapsible} from './utils/useCollapsible';
export {useCollapsible, Collapsible} from './utils/useCollapsible';
export type {
UseCollapsibleConfig,
UseCollapsibleReturns,
} from './utils/useCollapsible';
export {
MobileSecondaryMenuProvider,
MobileSecondaryMenuFiller,
useMobileSecondaryMenuRenderer,
} from './utils/mobileSecondaryMenu';
export type {MobileSecondaryMenuComponent} from './utils/mobileSecondaryMenu';
export {
useDocsPreferredVersion,
useDocsPreferredVersionByPluginId,

View file

@ -0,0 +1,111 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
useState,
ReactNode,
useContext,
createContext,
useEffect,
ComponentType,
useMemo,
} from 'react';
/*
The idea behind all this is that a specific component must be able to fill a placeholder in the generic layout
The doc page should be able to fill the secondary menu of the main mobile navbar.
This permits to reduce coupling between the main layout and the specific page.
This kind of feature is often called portal/teleport/gateway... various unmaintained React libs exist
Most up-to-date one: https://github.com/gregberge/react-teleporter
Not sure any of those is safe regarding concurrent mode.
*/
type ExtraProps = {
toggleSidebar: () => void;
};
export type MobileSecondaryMenuComponent<Props extends unknown> = ComponentType<
Props & ExtraProps
>;
type State = {
component: MobileSecondaryMenuComponent<unknown>;
props: unknown;
} | null;
function useContextValue() {
return useState<State>(null);
}
type ContextValue = ReturnType<typeof useContextValue>;
const Context = createContext<ContextValue | null>(null);
export function MobileSecondaryMenuProvider({children}: {children: ReactNode}) {
return (
<Context.Provider value={useContextValue()}>{children}</Context.Provider>
);
}
function useMobileSecondaryMenuContext(): ContextValue {
const value = useContext(Context);
if (value === null) {
throw new Error(
'MobileSecondaryMenuProvider was not used correctly, context value is null',
);
}
return value;
}
export function useMobileSecondaryMenuRenderer(): (
extraProps: ExtraProps,
) => ReactNode | undefined {
const [state] = useMobileSecondaryMenuContext();
if (state) {
const Comp = state.component;
return function render(extraProps) {
return <Comp {...state.props} {...extraProps} />;
};
}
return () => undefined;
}
function useShallowMemoizedObject<O extends Record<string, unknown>>(obj: O) {
return useMemo(
() => obj,
// Is this safe?
[...Object.keys(obj), ...Object.values(obj)],
);
}
// Fill the secondary menu placeholder with some real content
export function MobileSecondaryMenuFiller<
Props extends Record<string, unknown>
>({
component,
props,
}: {
component: MobileSecondaryMenuComponent<Props & ExtraProps>;
props: Props;
}): JSX.Element | null {
const [, setState] = useMobileSecondaryMenuContext();
// To avoid useless context re-renders, props are memoized shallowly
const memoizedProps = useShallowMemoizedObject(props);
useEffect(() => {
// @ts-expect-error: context is not 100% typesafe but it's ok
setState({component, props: memoizedProps});
}, [setState, component, memoizedProps]);
useEffect(() => {
return () => setState(null);
}, [setState]);
return null;
}

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
import React, {
useState,
useEffect,
useRef,
@ -13,46 +13,38 @@ import {
RefObject,
Dispatch,
SetStateAction,
TransitionEvent,
ReactNode,
} from 'react';
/*
Lex111: Dynamic transition duration is used in Material deisign, this technique is good for a large number of items.
https://material.io/archive/guidelines/motion/duration-easing.html#duration-easing-dynamic-durations
https://github.com/mui-org/material-ui/blob/e724d98eba018e55e1a684236a2037e24bcf050c/packages/material-ui/src/styles/createTransitions.js#L40-L43
*/
function getAutoHeightDuration(height: number) {
const constant = height / 36;
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
}
type CollapsibleAnimationConfig = {
duration?: number;
easing?: string;
};
const DefaultAnimationEasing = 'ease-in-out';
export type UseCollapsibleConfig = {
initialState: boolean | (() => boolean);
animation?: CollapsibleAnimationConfig;
};
export type UseCollapsibleReturns = {
collapsed: boolean;
setCollapsed: Dispatch<SetStateAction<boolean>>;
toggleCollapsed: () => void;
getToggleProps(): {
onClick?: () => void;
};
getCollapsibleProps(): {
ref: RefObject<any>; // any because TS is a pain for HTML element refs, see https://twitter.com/sebastienlorber/status/1412784677795110914
onTransitionEnd: (e: TransitionEvent) => void;
};
};
// This hook just define the state
export function useCollapsible({
initialState,
}: UseCollapsibleConfig): UseCollapsibleReturns {
const [collapsed, setCollapsed] = useState(initialState ?? false);
const toggleCollapsed = useCallback(() => {
setCollapsed((expanded) => !expanded);
}, []);
return {
collapsed,
setCollapsed,
toggleCollapsed,
};
}
const CollapsedStyles = {
display: 'none',
overflow: 'hidden',
@ -72,6 +64,21 @@ function applyCollapsedStyle(el: HTMLElement, collapsed: boolean) {
el.style.height = collapsedStyles.height;
}
/*
Lex111: Dynamic transition duration is used in Material design, this technique is good for a large number of items.
https://material.io/archive/guidelines/motion/duration-easing.html#duration-easing-dynamic-durations
https://github.com/mui-org/material-ui/blob/e724d98eba018e55e1a684236a2037e24bcf050c/packages/material-ui/src/styles/createTransitions.js#L40-L43
*/
function getAutoHeightDuration(height: number) {
const constant = height / 36;
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
}
type CollapsibleAnimationConfig = {
duration?: number;
easing?: string;
};
function useCollapseAnimation({
collapsibleRef,
collapsed,
@ -135,41 +142,39 @@ function useCollapseAnimation({
}, [collapsibleRef, collapsed, animation]);
}
/*
This hook encapsulate the animated collapsible behavior
You have to apply the getToggleProps + getCollapsibleProps wire everything
Similar to other solutions in the React ecosystem, like Downshift for Selects
*/
export function useCollapsible({
initialState,
type CollapsibleElementType = React.ElementType<
Pick<React.HTMLAttributes<unknown>, 'className' | 'onTransitionEnd'>
>;
export function Collapsible({
as: As = 'div',
collapsed,
children,
animation,
}: UseCollapsibleConfig): UseCollapsibleReturns {
const collapsibleRef = useRef<HTMLElement>(null);
const [collapsed, setCollapsed] = useState(initialState ?? false);
const toggleCollapsed = useCallback(() => {
setCollapsed((expanded) => !expanded);
}, []);
className,
}: {
as?: CollapsibleElementType; // TODO better typing, allow any html element (keyof JSX.IntrinsicElement => not working)
collapsed: boolean;
children: ReactNode;
animation?: CollapsibleAnimationConfig;
className?: string;
}) {
// any because TS is a pain for HTML element refs, see https://twitter.com/sebastienlorber/status/1412784677795110914
const collapsibleRef = useRef<any>(null);
useCollapseAnimation({collapsibleRef, collapsed, animation});
return {
collapsed,
setCollapsed,
toggleCollapsed,
getToggleProps: () => ({
onClick: toggleCollapsed,
}),
getCollapsibleProps: () => ({
ref: collapsibleRef,
onTransitionEnd: (e) => {
return (
<As
// @ts-expect-error: see https://twitter.com/sebastienlorber/status/1412784677795110914
ref={collapsibleRef}
onTransitionEnd={(e) => {
if (e.propertyName === 'height') {
applyCollapsedStyle(collapsibleRef.current!, collapsed);
}
},
}),
};
}}
className={className}>
{children}
</As>
);
}

View file

@ -103,6 +103,7 @@ export type ThemeConfig = {
hideableSidebar: boolean;
image: string;
metadatas: Array<Record<string, string>>;
sidebarCollapsible: boolean;
};
export function useThemeConfig(): ThemeConfig {

View file

@ -346,6 +346,7 @@ export interface RouteConfig {
routes?: RouteConfig[];
exact?: boolean;
priority?: number;
[propName: string]: any;
}
// Aliases used for Webpack resolution (when using docusaurus swizzle)

View file

@ -38,16 +38,17 @@ Object {
"routesConfig": "
import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator';
export default [
{
path: '/blog',
component: ComponentCreator('/blog','94e'),
exact: true,
},
{
path: '*',
component: ComponentCreator('*')
}
{
path: '/blog',
component: ComponentCreator('/blog','94e'),
exact: true
},
{
path: '*',
component: ComponentCreator('*')
}
];
",
"routesPaths": Array [
@ -90,16 +91,16 @@ Object {
},
},
"routesChunkNames": Object {
"/docs/hello-f94": Object {
"/docs/hello-44b": Object {
"component": "component---theme-doc-item-178-a40",
"content": "content---docs-helloaff-811",
"metadata": "metadata---docs-hello-956-741",
},
"/docs:route-838": Object {
"/docs:route-63b": Object {
"component": "component---theme-doc-page-1-be-9be",
"docsMetadata": "docsMetadata---docs-routef-34-881",
},
"docs/foo/baz-f88": Object {
"docs/foo/baz-ac2": Object {
"component": "component---theme-doc-item-178-a40",
"content": "content---docs-foo-baz-8-ce-61e",
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
@ -108,28 +109,29 @@ Object {
"routesConfig": "
import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator';
export default [
{
path: '/docs:route',
component: ComponentCreator('/docs:route','838'),
routes: [
{
path: '/docs/hello',
component: ComponentCreator('/docs/hello','f94'),
exact: true,
},
{
path: 'docs/foo/baz',
component: ComponentCreator('docs/foo/baz','f88'),
},
]
},
{
path: '*',
component: ComponentCreator('*')
}
{
path: '/docs:route',
component: ComponentCreator('/docs:route','63b'),
routes: [
{
path: '/docs/hello',
component: ComponentCreator('/docs/hello','44b'),
exact: true,
'sidebar': \\"main\\"
},
{
path: 'docs/foo/baz',
component: ComponentCreator('docs/foo/baz','ac2'),
'sidebar': \\"secondary\\"
}
]
},
{
path: '*',
component: ComponentCreator('*')
}
];
",
"routesPaths": Array [
@ -156,16 +158,16 @@ Object {
"routesConfig": "
import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator';
export default [
{
path: '',
component: ComponentCreator('','b2a'),
},
{
path: '*',
component: ComponentCreator('*')
}
{
path: '',
component: ComponentCreator('','b2a')
},
{
path: '*',
component: ComponentCreator('*')
}
];
",
"routesPaths": Array [

View file

@ -25,6 +25,7 @@ describe('loadRoutes', () => {
content: 'docs/hello.md',
metadata: 'docs-hello-da2.json',
},
sidebar: 'main',
},
{
path: 'docs/foo/baz',
@ -33,6 +34,7 @@ describe('loadRoutes', () => {
content: 'docs/foo/baz.md',
metadata: 'docs-foo-baz-dd9.json',
},
sidebar: 'secondary',
},
],
};

View file

@ -21,30 +21,52 @@ import {
ChunkNames,
} from '@docusaurus/types';
function indent(str: string) {
const spaces = ' ';
return `${spaces}${str.replace(/(\n)/g, `\n${spaces}`)}`;
}
const createRouteCodeString = ({
routePath,
routeHash,
exact,
subroutesCodeStrings,
props,
}: {
routePath: string;
routeHash: string;
exact?: boolean;
subroutesCodeStrings?: string[];
props: {[propName: string]: any};
}) => {
const str = `{
path: '${routePath}',
component: ComponentCreator('${routePath}','${routeHash}'),
${exact ? `exact: true,` : ''}
${
subroutesCodeStrings
? ` routes: [
${removeSuffix(subroutesCodeStrings.join(',\n'), ',\n')},
]
`
: ''
}}`;
return str;
const parts = [
`path: '${routePath}'`,
`component: ComponentCreator('${routePath}','${routeHash}')`,
];
if (exact) {
parts.push(`exact: true`);
}
if (subroutesCodeStrings) {
parts.push(
`routes: [
${indent(removeSuffix(subroutesCodeStrings.join(',\n'), ',\n'))}
]`,
);
}
Object.entries(props).forEach(([propName, propValue]) => {
// Figure out how to "unquote" JS attributes that don't need to be quoted
// Is this lib reliable? https://github.com/armanozak/should-quote
const shouldQuote = true; // TODO
const key = shouldQuote ? `'${propName}'` : propName;
parts.push(`${key}: ${JSON.stringify(propValue)}`);
});
return `{
${indent(parts.join(',\n'))}
}`;
};
const NotFoundRouteCode = `{
@ -106,6 +128,9 @@ export default async function loadRoutes(
modules = {},
routes: subroutes,
exact,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
priority,
...props
} = routeConfig;
if (!isString(routePath) || !component) {
@ -138,15 +163,18 @@ export default async function loadRoutes(
routeHash,
exact,
subroutesCodeStrings: subroutes?.map(generateRouteCode),
props,
});
}
const routesConfig = `
${RoutesImportsCode}
export default [
${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},
${NotFoundRouteCode}
];\n`;
${indent(`${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},`)}
${indent(NotFoundRouteCode)}
];
`;
return {
registry,

View file

@ -310,6 +310,7 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING || isI18nStaging;
liveCodeBlock: {
playgroundPosition: 'bottom',
},
sidebarCollapsible: true,
hideableSidebar: true,
colorMode: {
defaultMode: 'light',