diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index b0a53998ede..6aa2b17f985 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -212,6 +212,7 @@ const config: Config = { require.resolve('./src/css/index.scss'), require.resolve('./src/css/showcase.scss'), require.resolve('./src/css/versions.scss'), + require.resolve('./src/css/docs-secondary-nav.scss'), ], }, gtag: { @@ -426,31 +427,14 @@ const config: Config = { style: 'dark', items: [ { - label: 'Development', type: 'dropdown', + label: 'Docs', position: 'right', items: [ - { - label: 'Guides', - type: 'doc', - docId: 'getting-started', - }, - { - label: 'Components', - type: 'doc', - docId: 'components-and-apis', - }, - { - label: 'APIs', - type: 'doc', - docId: 'accessibilityinfo', - }, - { - label: 'Architecture', - type: 'doc', - docId: 'architecture-overview', - docsPluginId: 'architecture', - }, + {label: 'Guides', to: '/docs/getting-started'}, + {label: 'Components', to: '/docs/components-and-apis'}, + {label: 'APIs', to: '/docs/accessibilityinfo'}, + {label: 'Architecture', to: '/architecture/overview'}, ], }, { @@ -477,17 +461,6 @@ const config: Config = { label: 'Blog', position: 'right', }, - { - type: 'docsVersionDropdown', - position: 'left', - dropdownActiveClassDisabled: true, - dropdownItemsAfter: [ - { - to: '/versions', - label: 'All versions', - }, - ], - }, { href: 'https://github.com/facebook/react-native', 'aria-label': 'GitHub repository', diff --git a/website/sidebarsCommunity.ts b/website/sidebarsCommunity.ts index cbe83d78989..d517a6794b6 100644 --- a/website/sidebarsCommunity.ts +++ b/website/sidebarsCommunity.ts @@ -1,13 +1,5 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; export default { - community: [ - { - type: 'category', - label: 'Community', - collapsed: false, - collapsible: false, - items: ['overview', 'staying-updated', 'communities', 'support'], - }, - ], + community: ['overview', 'staying-updated', 'communities', 'support'], } satisfies SidebarsConfig; diff --git a/website/src/components/DocsSecondaryNav/index.tsx b/website/src/components/DocsSecondaryNav/index.tsx new file mode 100644 index 00000000000..79f1b53f94d --- /dev/null +++ b/website/src/components/DocsSecondaryNav/index.tsx @@ -0,0 +1,247 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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, useRef, useState} from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import {useLocation} from '@docusaurus/router'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client'; +import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem'; +import CopyPageButton from 'docusaurus-plugin-copy-page-button/react'; + +import styles from './styles.module.css'; + +type SectionLink = {label: string; to: string}; + +// The section dropdown mirrors the navbar "Docs" dropdown; read its entries from +// the site config so the two can't drift. +function useSectionLinks(): readonly SectionLink[] { + const {siteConfig} = useDocusaurusContext(); + const navbar = siteConfig.themeConfig.navbar as { + items?: ReadonlyArray<{ + type?: string; + label?: string; + items?: SectionLink[]; + }>; + }; + return ( + navbar.items?.find( + item => item.type === 'dropdown' && item.label === 'Docs' + )?.items ?? [] + ); +} + +// Guides, Components and APIs all share the /docs/ URL space, so the active +// section is read from the current doc's sidebar rather than the pathname. +const SECTION_BY_SIDEBAR: Record = { + docs: 'Guides', + components: 'Components', + api: 'APIs', + architecture: 'Architecture', +}; + +type Section = { + title: string; + // The active entry to show on the dropdown toggle, or null to hide the + // dropdown entirely (sections outside the Docs area). + activeSectionLink: string | null; + // A "return to index" link shown to the right of the heading, or null. + backLink: {label: string; href: string} | null; + showCopyButton: boolean; +}; + +function getSection( + pathname: string, + sidebarName: string | undefined +): Section { + if (pathname.startsWith('/blog')) { + // The index and pagination are the blog listing itself; tags, archive, + // authors and individual posts all link back to it via "All posts". + const isBlogIndex = + pathname === '/blog' || + pathname === '/blog/' || + pathname.startsWith('/blog/page/'); + // Copy page is only meaningful on an individual post, not on the listing or + // the other list-style blog routes (tags, archive, authors). + const isBlogPost = + !isBlogIndex && + !pathname.startsWith('/blog/tags') && + !pathname.startsWith('/blog/archive') && + !pathname.startsWith('/blog/authors'); + return { + title: 'Blog', + activeSectionLink: null, + backLink: isBlogIndex ? null : {label: 'All posts', href: '/blog'}, + showCopyButton: isBlogPost, + }; + } + if (pathname.startsWith('/community')) { + return { + title: 'Community', + activeSectionLink: null, + backLink: null, + showCopyButton: true, + }; + } + if (pathname.startsWith('/contributing')) { + return { + title: 'Contributing', + activeSectionLink: null, + backLink: null, + showCopyButton: true, + }; + } + // Docs area (main docs + architecture): the active section follows the doc's + // sidebar (Guides / Components / APIs / Architecture). + return { + title: 'Docs', + activeSectionLink: sidebarName + ? (SECTION_BY_SIDEBAR[sidebarName] ?? null) + : null, + backLink: null, + showCopyButton: true, + }; +} + +function SectionDropdown({ + activeLabel, + links, +}: { + activeLabel: string; + links: readonly SectionLink[]; +}) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!open) { + return undefined; + } + const onDocumentClick = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + document.addEventListener('click', onDocumentClick); + return () => document.removeEventListener('click', onDocumentClick); + }, [open]); + + return ( +
+ +
    + {links.map(link => ( +
  • + setOpen(false)}> + {link.label} + +
  • + ))} +
+
+ ); +} + +export default function DocsSecondaryNav({ + sidebarName, +}: { + sidebarName?: string; +}) { + const {pathname} = useLocation(); + const sectionLinks = useSectionLinks(); + const {title, activeSectionLink, backLink, showCopyButton} = getSection( + pathname, + sidebarName + ); + // The docs version dropdown belongs to the main docs; it self-hides elsewhere. + const {activeVersion, activeDoc} = useActiveDocContext('default'); + const showVersionDropdown = Boolean(activeDoc); + const isNextVersion = activeVersion?.name === 'current'; + + return ( + + ); +} diff --git a/website/src/components/DocsSecondaryNav/styles.module.css b/website/src/components/DocsSecondaryNav/styles.module.css new file mode 100644 index 00000000000..04dedf7cd2c --- /dev/null +++ b/website/src/components/DocsSecondaryNav/styles.module.css @@ -0,0 +1,181 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.secondaryNav { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: var(--rn-subnav-height); + padding: 0 1.5rem; + background-color: var(--ifm-background-color); + + /* Pin directly below the main navbar so page content scrolls underneath. */ + position: sticky; + top: var(--ifm-navbar-height); + z-index: calc(var(--ifm-z-index-fixed) - 1); +} + +/* Bottom keyline, inset from both edges so it comes in from the screen edges + without lining up with the inner elements' padding. */ +.secondaryNav:after { + content: ""; + position: absolute; + right: 0.75rem; + bottom: 0; + left: 0.75rem; + height: 1px; + background-color: var(--ifm-toc-border-color); +} + +.start { + display: flex; + align-items: center; + gap: 1rem; + min-width: 0; +} + +.end { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.title { + font-size: 1.5rem; + font-weight: var(--ifm-heading-font-weight); + line-height: 1.2; +} + +/* Outlined controls: the section dropdown toggle and the back link share the + bordered box style of the Copy page button. */ + +.dropdown { + font-size: 0.9rem; +} + +.dropdownToggle, +.backLink { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.6rem; + font-family: inherit; + font-size: 0.9rem; + color: var(--ifm-font-color-base); + background: transparent; + border: 1px solid var(--ifm-color-gray-500); + border-radius: var(--ifm-global-radius); + text-decoration: none; +} + +.dropdownToggle { + cursor: pointer; +} + +.dropdownToggle:after { + content: ""; + display: inline-block; + width: 0; + height: 0; + border-left: 0.3rem solid transparent; + border-right: 0.3rem solid transparent; + border-top: 0.3rem solid currentcolor; + opacity: 0.6; + transform: translateY(1px); +} + +.backLink { + /* Trim vertical padding so the taller chevron doesn't inflate the box, and + tighten the inset before the icon. Match the Copy page button weight. */ + padding: 0.2rem 0.6rem 0.2rem 0.4rem; + font-weight: 500; +} + +.backChevron { + opacity: 0.75; +} + +.dropdownToggle:hover, +.backLink:hover { + color: var(--ifm-font-color-base); + text-decoration: none; + background-color: var(--ifm-menu-color-background-hover); + border-color: var(--ifm-color-gray-400); +} + +/* Docs version dropdown wrapper. The dropdown reuses the theme navbar dropdown; + its pill styling lives in docs-secondary-nav.scss (it targets the theme's + global navbar classes). */ + +.versionDropdown { + display: flex; + align-items: center; + + /* Sit closer to the section dropdown than the .start gap allows. */ + margin-left: -0.5rem; +} + +/* Copy page button — appearance shared with its former home in DocItem */ + +.copyPageContainer { + margin-bottom: 0; +} + +button.copyPageButton { + border-radius: var(--ifm-global-radius); + border-color: var(--ifm-color-gray-500); +} + +button.copyPageButton:hover { + background-color: var(--ifm-menu-color-background-hover) !important; + border-color: var(--ifm-color-gray-400) !important; +} + +button.copyPageButton svg { + opacity: 0.75; +} + +.copyPageDropdown { + border-color: var(--ifm-table-border-color) !important; +} + +.copyPageDropdown button[class^="dropdownItem"] { + border-bottom-color: var(--ifm-table-border-color) !important; +} + +.copyPageDropdown button[class^="dropdownItem"]:hover { + background-color: var(--ifm-menu-color-background-hover); +} + +.copyPageDropdown div[class^="itemDescription"] { + font-weight: 300; + font-size: 12px; +} + +[data-theme="dark"] .dropdownToggle:hover, +[data-theme="dark"] .backLink:hover { + background-color: var(--dark); + border-color: var(--ifm-color-gray-800); +} + +[data-theme="dark"] button.copyPageButton:hover { + background-color: var(--dark) !important; + border-color: var(--ifm-color-gray-800) !important; +} + +[data-theme="dark"] .copyPageDropdown { + background-color: var(--ifm-navbar-background-color); +} + +[data-theme="dark"] .copyPageDropdown button[class^="dropdownItem"]:hover { + background-color: var(--dark); +} + +[data-theme="dark"] .copyPageDropdown div[class^="itemDescription"] { + color: var(--ifm-color-gray-600) !important; +} diff --git a/website/src/css/customTheme.scss b/website/src/css/customTheme.scss index b08da1ee456..269bf2a7939 100644 --- a/website/src/css/customTheme.scss +++ b/website/src/css/customTheme.scss @@ -2257,13 +2257,21 @@ html[data-theme="dark"] .docsRating { /* Blog */ +// Keep blog anchors clear of the sticky DocsSecondaryNav. +html.blog-wrapper { + scroll-padding-top: calc(var(--ifm-navbar-height) + var(--rn-subnav-height)); +} + .blog-wrapper .main-wrapper { h3[class*="sidebarItemTitle"] { color: var(--ifm-font-color-secondary); } div[class*="tableOfContents"] { - top: calc(var(--ifm-navbar-height)); + top: calc(var(--ifm-navbar-height) + var(--rn-subnav-height)); + max-height: calc( + 100vh - (var(--ifm-navbar-height) + var(--rn-subnav-height) + 2rem) + ); } .table-of-contents { diff --git a/website/src/css/docs-secondary-nav.scss b/website/src/css/docs-secondary-nav.scss new file mode 100644 index 00000000000..f8fbcf66d02 --- /dev/null +++ b/website/src/css/docs-secondary-nav.scss @@ -0,0 +1,115 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Offsets that keep the sticky doc sidebar, table of contents and anchor + * scrolling clear of the sticky DocsSecondaryNav. Scoped to `.plugin-docs`, the + * class Docusaurus sets on for every content-docs page (the pages that + * render the secondary nav). + */ + +:root { + --rn-subnav-height: 3rem; +} + +html.plugin-docs { + scroll-padding-top: calc(var(--ifm-navbar-height) + var(--rn-subnav-height)); +} + +@media (min-width: 997px) { + // Position the sticky sidebar like the blog sidebar: drop the theme's + // negative margin and instead stick the viewport just below the secondary + // nav. This keeps the first item's normal top spacing at every scroll + // position (the negative-margin mechanism otherwise drifts once a second + // sticky bar is added above it). + html.plugin-docs aside[class^="theme-doc-sidebar-container"] { + margin-top: 0; + } + + html.plugin-docs aside[class^="theme-doc-sidebar-container"] > div { + top: calc(var(--ifm-navbar-height) + var(--rn-subnav-height)); + max-height: calc( + 100vh - (var(--ifm-navbar-height) + var(--rn-subnav-height)) + ); + } + + // The viewport now clears both bars, so the content box no longer needs the + // theme's navbar-height top padding. + html.plugin-docs aside[class^="theme-doc-sidebar-container"] > div > div { + padding-top: 0; + } + + html.plugin-docs div[class^="tableOfContents"] { + top: calc(var(--ifm-navbar-height) + var(--rn-subnav-height) + 34px); + max-height: calc( + 100vh - (var(--ifm-navbar-height) + var(--rn-subnav-height) + 2rem) + ); + } +} + +/** + * Docs version dropdown. Reuses the theme navbar dropdown, styled to match the + * other outlined controls in this nav but with fully rounded corners for + * differentiation. Only the "Next" version is highlighted in the brand blue; + * released versions use the same white/grey outline as the other dropdowns. + */ + +[class*="versionDropdown"] { + .navbar__item { + // Keep it visible on mobile too — Infima hides navbar items below 996px. + display: inline-flex; + margin: 0; + padding: 0; + } + + .navbar__link { + display: inline-flex; + align-items: center; + margin: 0; + padding: 0.35rem 0.6rem; + font-size: 0.9rem; + font-weight: var(--ifm-font-weight-normal); + + // Match the section dropdown button's compact height (it uses the browser + // default line-height rather than the taller inherited base). + line-height: normal; + color: var(--ifm-font-color-base); + background: transparent; + border: 1px solid var(--ifm-color-gray-500); + border-radius: 999px; + + // Match the section dropdown caret: nudged down and dimmed to grey. + &:after { + opacity: 0.6; + transform: translateY(-1px); + } + + &:hover { + color: var(--ifm-font-color-base); + background-color: var(--ifm-menu-color-background-hover); + border-color: var(--ifm-color-gray-400); + } + } +} + +[data-theme="dark"] [class*="versionDropdown"] .navbar__link:hover { + background-color: var(--dark); + border-color: var(--ifm-color-gray-800); +} + +[class*="versionDropdown"][data-version-next] .navbar__link { + color: var(--ifm-button-color, #fff); + background-color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + + &:hover { + color: var(--ifm-button-color, #fff); + background-color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + filter: brightness(0.92); + } +} diff --git a/website/src/theme/BlogLayout/index.tsx b/website/src/theme/BlogLayout/index.tsx new file mode 100644 index 00000000000..7bb44c0730c --- /dev/null +++ b/website/src/theme/BlogLayout/index.tsx @@ -0,0 +1,40 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {Props} from '@theme/BlogLayout'; + +import React from 'react'; +import clsx from 'clsx'; +import Layout from '@theme/Layout'; +import BlogSidebar from '@theme/BlogSidebar'; +import DocsSecondaryNav from '@site/src/components/DocsSecondaryNav'; + +// Ejected from @docusaurus/theme-classic to render the shared secondary nav +// full-width below the main navbar, above the blog content. +export default function BlogLayout(props: Props) { + const {sidebar, toc, children, ...layoutProps} = props; + const hasSidebar = sidebar && sidebar.items.length > 0; + + return ( + + +
+
+ +
+ {children} +
+ {toc &&
{toc}
} +
+
+
+ ); +} diff --git a/website/src/theme/BlogSidebar/Desktop/index.tsx b/website/src/theme/BlogSidebar/Desktop/index.tsx index 157fd9c0b0a..bbbb05b1959 100644 --- a/website/src/theme/BlogSidebar/Desktop/index.tsx +++ b/website/src/theme/BlogSidebar/Desktop/index.tsx @@ -23,9 +23,6 @@ export default function BlogSidebarDesktop({sidebar}: Props) { message: 'Blog recent posts navigation', description: 'The ARIA label for recent posts in the blog sidebar', })}> -
- {sidebar.title} -