diff --git a/.vscode/settings.json b/.vscode/settings.json index e746d28ff4..40760fcf5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,8 @@ "tailwindCSS.classFunctions": ["clsx"], "editor.quickSuggestions": { "strings": "on" - } + }, + "typescript.preferences.autoImportFileExcludePatterns": [ + "**/node_modules/lucide-react" + ] } diff --git a/package.json b/package.json index b50ebe9a81..f85cd8e316 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-use-measure": "^2.1.7", "rehype-mermaid": "^3.0.0", "rss": "1.2.2", + "scroll-into-view-if-needed": "^3.1.0", "server-only": "0.0.1", "string-similarity": "^4.0.4", "string-strip-html": "^13.4.8", diff --git a/patches/nextra-theme-docs.patch b/patches/nextra-theme-docs.patch index b6375d172f..f9f3ee4f2e 100644 --- a/patches/nextra-theme-docs.patch +++ b/patches/nextra-theme-docs.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.d.mts b/dist/index.d.mts -index 71f87bcd1dde49d7c19ad49fc098e715a76c5c10..6671e29326be99861058895916185910452ced17 100644 +index 71f87bcd1dde49d7c19ad49fc098e715a76c5c10..aadd6228910ee3ebafccfae8672cb9ae1b0bca3c 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts -@@ -1421,3 +1421,19 @@ declare function ThemeSwitch({ lite, className }: ThemeSwitchProps): ReactElemen +@@ -1421,3 +1421,25 @@ declare function ThemeSwitch({ lite, className }: ThemeSwitchProps): ReactElemen declare function Layout({ children, themeConfig, pageOpts }: NextraThemeLayoutProps): ReactElement; export { Bleed, Collapse, type PartialDocsThemeConfig as DocsThemeConfig, Link, LocaleSwitch, Navbar, NotFoundPage, SkipNavContent, SkipNavLink, ThemeSwitch, Layout as default, getComponents, useConfig, useMenu, useThemeConfig }; @@ -22,8 +22,14 @@ index 71f87bcd1dde49d7c19ad49fc098e715a76c5c10..6671e29326be99861058895916185910 +export declare const useIntersectionObserver: () => IntersectionObserver | null +export declare const useSlugs: () => WeakMap + ++export declare const Breadcrumb: (props: { activePath: Item[] }) => ReactElement | null ++export declare const NavLinks: (props: NavLinkProps) => ReactElement | null ++export interface NavLinkProps { ++ currentIndex: number ++ flatDocsDirectories: Item[] ++} diff --git a/dist/index.js b/dist/index.js -index 56201641fd965dcc5ab7c5df53e444c41293c00e..860b8cfd297e82da041c4d8287ed266691d75a0e 100644 +index 56201641fd965dcc5ab7c5df53e444c41293c00e..07147c688ae75c4c7daf082833acc71de16b36ee 100644 --- a/dist/index.js +++ b/dist/index.js @@ -100,10 +100,10 @@ IntersectionObserverContext.displayName = "IntersectionObserver"; @@ -41,3 +47,21 @@ index 56201641fd965dcc5ab7c5df53e444c41293c00e..860b8cfd297e82da041c4d8287ed2666 var ActiveAnchorProvider = ({ children }) => { +@@ -526,7 +526,7 @@ import NextLink2 from "next/link"; + import { ArrowRightIcon } from "nextra/icons"; + import { Fragment as Fragment3 } from "react"; + import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime"; +-function Breadcrumb({ ++export function Breadcrumb({ + activePath + }) { + return /* @__PURE__ */ jsx9("div", { className: "nextra-breadcrumb _mt-1.5 _flex _items-center _gap-1 _overflow-hidden _text-sm _text-gray-500 dark:_text-gray-400 contrast-more:_text-current", children: activePath.map((item, index, arr) => { +@@ -1255,7 +1255,7 @@ var classes = { + ), + icon: cn10("_inline _h-5 _shrink-0") + }; +-function NavLinks({ ++export function NavLinks({ + flatDocsDirectories, + currentIndex + }) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab57921892..9263c223a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ patchedDependencies: hash: 6ssvmycplmexou7j5jtwzrnhmy path: patches/nextra.patch nextra-theme-docs: - hash: 6nt5xhmisiwamimp7yowa7aoxq + hash: 3vafyhtna4g6ghudqadsxldexi path: patches/nextra-theme-docs.patch importers: @@ -117,7 +117,7 @@ importers: version: 3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.23)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) nextra-theme-docs: specifier: 3.3.1 - version: 3.3.1(patch_hash=6nt5xhmisiwamimp7yowa7aoxq)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.23)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.3.1(patch_hash=3vafyhtna4g6ghudqadsxldexi)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.23)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) numbro: specifier: 2.5.0 version: 2.5.0 @@ -154,6 +154,9 @@ importers: rss: specifier: 1.2.2 version: 1.2.2 + scroll-into-view-if-needed: + specifier: ^3.1.0 + version: 3.1.0 server-only: specifier: 0.0.1 version: 0.0.1 @@ -10762,7 +10765,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.3.1(patch_hash=6nt5xhmisiwamimp7yowa7aoxq)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.23)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.3.1(patch_hash=3vafyhtna4g6ghudqadsxldexi)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=6ssvmycplmexou7j5jtwzrnhmy)(@types/react@18.3.23)(acorn@8.15.0)(next@14.2.32(@babel/core@7.28.3)(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 diff --git a/src/components/back-to-top.tsx b/src/components/back-to-top.tsx new file mode 100644 index 0000000000..f727dcea95 --- /dev/null +++ b/src/components/back-to-top.tsx @@ -0,0 +1,54 @@ +/** + * @file BackToTop component extracted from Nextra + */ + +import { clsx } from "clsx" +import { Button } from "nextra/components" +import { ArrowRightIcon } from "nextra/icons" +import type { ComponentProps, ReactElement, ReactNode } from "react" + +const SCROLL_TO_OPTIONS = { top: 0, behavior: "smooth" } as const + +const scrollToTop: ComponentProps<"button">["onClick"] = event => { + const buttonElement = event.currentTarget + const tocElement = buttonElement.parentElement!.parentElement! + + window.scrollTo(SCROLL_TO_OPTIONS) + tocElement.scrollTo(SCROLL_TO_OPTIONS) + + // Fixes https://github.com/facebook/react/issues/20770 + // Fixes https://github.com/shuding/nextra/issues/2917 + buttonElement.disabled = true +} + +export function BackToTop({ + children, + className, + hidden, +}: { + children: ReactNode + className?: string + hidden: boolean +}): ReactElement { + return ( + + ) +} diff --git a/src/components/nextra-mdx-wrapper.tsx b/src/components/nextra-mdx-wrapper.tsx new file mode 100644 index 0000000000..cb7a834abc --- /dev/null +++ b/src/components/nextra-mdx-wrapper.tsx @@ -0,0 +1,155 @@ +import type { ReactElement, ReactNode } from "react" +import { useMounted } from "nextra/hooks" +import { Heading } from "nextra" +import { + useConfig, + useThemeConfig, + SkipNavContent, + Breadcrumb, + NavLinks, +} from "nextra-theme-docs" +import { clsx } from "clsx" + +import { Sidebar } from "./sidebar" +import { renderComponent } from "./utils" +import { TableOfContents } from "./table-of-contents" + +const classes = { + toc: clsx("nextra-toc order-last max-xl:hidden w-64 shrink-0 print:hidden"), + main: clsx("w-full break-words"), +} + +export interface NextraMdxWrapperProps { + toc?: Heading[] + children?: React.ReactNode +} + +export function NextraMdxWrapper({ + toc = [], + children, +}: NextraMdxWrapperProps) { + const config = useConfig() + const { + activeType, + activeThemeContext: themeContext, + docsDirectories, + directories, + } = config.normalizePagesResult + + const tocEl = + activeType === "page" || + !themeContext.toc || + themeContext.layout !== "default" ? ( + themeContext.layout !== "full" && + themeContext.layout !== "raw" && ( +