chore: update to Fumadocs 15.7 (#4154)

Co-authored-by: KinfeMichael Tariku <65047246+Kinfe123@users.noreply.github.com>
Co-authored-by: Kinfe123 <kinfishtech@gmail.com>
This commit is contained in:
Fuma Nama
2025-08-23 10:00:45 +08:00
committed by GitHub
parent c45ad901ce
commit d20f9a763c
32 changed files with 6469 additions and 4588 deletions

View File

@@ -15,7 +15,6 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
import { File, Folder, Files } from "fumadocs-ui/components/files"; import { File, Folder, Files } from "fumadocs-ui/components/files";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
import { Pre } from "fumadocs-ui/components/codeblock"; import { Pre } from "fumadocs-ui/components/codeblock";
import { DocsBody } from "fumadocs-ui/page";
import { Glow } from "../_components/default-changelog"; import { Glow } from "../_components/default-changelog";
import { IconLink } from "../_components/changelog-layout"; import { IconLink } from "../_components/changelog-layout";
import { BookIcon, GitHubIcon, XIcon } from "../_components/icons"; import { BookIcon, GitHubIcon, XIcon } from "../_components/icons";
@@ -23,6 +22,7 @@ import { DiscordLogoIcon } from "@radix-ui/react-icons";
import { StarField } from "../_components/stat-field"; import { StarField } from "../_components/stat-field";
import Image from "next/image"; import Image from "next/image";
import { BlogPage } from "../_components/blog-list"; import { BlogPage } from "../_components/blog-list";
import { Callout } from "@/components/ui/callout";
const metaTitle = "Blogs"; const metaTitle = "Blogs";
const metaDescription = "Latest changes , fixes and updates."; const metaDescription = "Latest changes , fixes and updates.";
@@ -119,7 +119,7 @@ export default async function Page({
</div> </div>
<div className="flex-1 min-h-0 h-screen overflow-y-auto px-4 relative md:px-8 pb-12 md:py-12"> <div className="flex-1 min-h-0 h-screen overflow-y-auto px-4 relative md:px-8 pb-12 md:py-12">
<div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div> <div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div>
<DocsBody> <div className="prose">
<MDX <MDX
components={{ components={{
...defaultMdxComponents, ...defaultMdxComponents,
@@ -151,9 +151,22 @@ export default async function Page({
DatabaseTable, DatabaseTable,
Accordion, Accordion,
Accordions, Accordions,
Callout: ({
children,
type,
...props
}: {
children: React.ReactNode;
type?: "info" | "warn" | "error" | "success" | "warning";
[key: string]: any;
}) => (
<Callout type={type} {...props}>
{children}
</Callout>
),
}} }}
/> />
</DocsBody> </div>
</div> </div>
</div> </div>
); );

View File

@@ -15,12 +15,12 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
import { File, Folder, Files } from "fumadocs-ui/components/files"; import { File, Folder, Files } from "fumadocs-ui/components/files";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
import { Pre } from "fumadocs-ui/components/codeblock"; import { Pre } from "fumadocs-ui/components/codeblock";
import { DocsBody } from "fumadocs-ui/page";
import ChangelogPage, { Glow } from "../_components/default-changelog"; import ChangelogPage, { Glow } from "../_components/default-changelog";
import { IconLink } from "../_components/changelog-layout"; import { IconLink } from "../_components/changelog-layout";
import { XIcon } from "../_components/icons"; import { XIcon } from "../_components/icons";
import { StarField } from "../_components/stat-field"; import { StarField } from "../_components/stat-field";
import { GridPatterns } from "../_components/grid-pattern"; import { GridPatterns } from "../_components/grid-pattern";
import { Callout } from "@/components/ui/callout";
const metaTitle = "Changelogs"; const metaTitle = "Changelogs";
const metaDescription = "Latest changes , fixes and updates."; const metaDescription = "Latest changes , fixes and updates.";
@@ -71,7 +71,7 @@ export default async function Page({
</div> </div>
<div className="px-4 relative md:px-8 pb-12 md:py-12"> <div className="px-4 relative md:px-8 pb-12 md:py-12">
<div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div> <div className="absolute top-0 left-0 h-full -translate-x-full w-px bg-gradient-to-b from-black/5 dark:from-white/10 via-black/3 dark:via-white/5 to-transparent"></div>
<DocsBody className="pt-8 md:pt-0"> <div className="prose pt-8 md:pt-0">
<MDX <MDX
components={{ components={{
...defaultMdxComponents, ...defaultMdxComponents,
@@ -103,9 +103,22 @@ export default async function Page({
DatabaseTable, DatabaseTable,
Accordion, Accordion,
Accordions, Accordions,
Callout: ({
children,
type,
...props
}: {
children: React.ReactNode;
type?: "info" | "warn" | "error" | "success" | "warning";
[key: string]: any;
}) => (
<Callout type={type} {...props}>
{children}
</Callout>
),
}} }}
/> />
</DocsBody> </div>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
import { source } from "@/lib/source"; import { source } from "@/lib/source";
import { DocsPage, DocsBody, DocsTitle } from "fumadocs-ui/page"; import { DocsPage, DocsBody, DocsTitle } from "@/components/docs/page";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { absoluteUrl } from "@/lib/utils"; import { absoluteUrl } from "@/lib/utils";
import DatabaseTable from "@/components/mdx/database-tables"; import DatabaseTable from "@/components/mdx/database-tables";
@@ -13,18 +13,22 @@ import { Features } from "@/components/blocks/features";
import { ForkButton } from "@/components/fork-button"; import { ForkButton } from "@/components/fork-button";
import Link from "next/link"; import Link from "next/link";
import defaultMdxComponents from "fumadocs-ui/mdx"; import defaultMdxComponents from "fumadocs-ui/mdx";
import {
CodeBlock,
Pre,
CodeBlockTab,
CodeBlockTabsList,
CodeBlockTabs,
} from "@/components/ui/code-block";
import { File, Folder, Files } from "fumadocs-ui/components/files"; import { File, Folder, Files } from "fumadocs-ui/components/files";
import { AutoTypeTable } from "fumadocs-typescript/ui"; import { AutoTypeTable } from "fumadocs-typescript/ui";
import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
import { Card, Cards } from "fumadocs-ui/components/card";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { contents } from "@/components/sidebar-content";
import { Endpoint } from "@/components/endpoint"; import { Endpoint } from "@/components/endpoint";
import { DividerText } from "@/components/divider-text"; import { DividerText } from "@/components/divider-text";
import { APIMethod } from "@/components/api-method"; import { APIMethod } from "@/components/api-method";
import { LLMCopyButton, ViewOptions } from "./page.client"; import { LLMCopyButton, ViewOptions } from "./page.client";
import { GenerateAppleJwt } from "@/components/generate-apple-jwt"; import { GenerateAppleJwt } from "@/components/generate-apple-jwt";
import { Callout } from "@/components/ui/callout";
export default async function Page({ export default async function Page({
params, params,
}: { }: {
@@ -37,8 +41,6 @@ export default async function Page({
notFound(); notFound();
} }
const { nextPage, prevPage } = getPageLinks(page.url);
const MDX = page.data.body; const MDX = page.data.body;
const avoidLLMHeader = ["Introduction", "Comparison"]; const avoidLLMHeader = ["Introduction", "Comparison"];
return ( return (
@@ -49,16 +51,11 @@ export default async function Page({
owner: "better-auth", owner: "better-auth",
repo: "better-auth", repo: "better-auth",
sha: "main", sha: "main",
path: `/docs/content/docs/${page.file.path}`, path: `/docs/content/docs/${page.path}`,
}} }}
tableOfContent={{ tableOfContent={{
style: "clerk",
header: <div className="w-10 h-4"></div>, header: <div className="w-10 h-4"></div>,
}} }}
footer={{
enabled: true,
component: <div className="w-10 h-4" />,
}}
> >
<DocsTitle>{page.data.title}</DocsTitle> <DocsTitle>{page.data.title}</DocsTitle>
{!avoidLLMHeader.includes(page.data.title) && ( {!avoidLLMHeader.includes(page.data.title) && (
@@ -74,6 +71,38 @@ export default async function Page({
<MDX <MDX
components={{ components={{
...defaultMdxComponents, ...defaultMdxComponents,
CodeBlockTabs: (props) => {
return (
<CodeBlockTabs
{...props}
className="bg-fd-secondary border-b p-0 rounded-lg"
>
<div {...props}>{props.children}</div>
</CodeBlockTabs>
);
},
CodeBlockTabsList: (props) => {
return (
<CodeBlockTabsList
{...props}
className="bg-fd-secondary my-0 pb-0 rounded-lg"
/>
);
},
CodeBlockTab: (props) => {
return <CodeBlockTab {...props} className="p-0 m-0 rounded-lg" />;
},
pre: (props) => {
return (
<CodeBlock className="bg-fd-muted rounded-xl" {...props}>
<div style={{ minWidth: "100%", display: "table" }}>
<Pre className="bg-fd-muted py-3 px-0 focus-visible:outline-none">
{props.children}
</Pre>
</div>
</CodeBlock>
);
},
Link: ({ Link: ({
className, className,
...props ...props
@@ -105,19 +134,18 @@ export default async function Page({
Accordions, Accordions,
Endpoint, Endpoint,
APIMethod, APIMethod,
Callout: ({ children, ...props }) => ( Callout: ({
<defaultMdxComponents.Callout children,
{...props} type,
className={cn( ...props
props, }: {
"bg-none rounded-none border-dashed border-border", children: React.ReactNode;
props.type === "info" && "border-l-blue-500/50", type?: "info" | "warn" | "error" | "success" | "warning";
props.type === "warn" && "border-l-amber-700/50", [key: string]: any;
props.type === "error" && "border-l-red-500/50", }) => (
)} <Callout type={type} {...props}>
>
{children} {children}
</defaultMdxComponents.Callout> </Callout>
), ),
DividerText, DividerText,
iframe: (props) => ( iframe: (props) => (
@@ -125,48 +153,12 @@ export default async function Page({
), ),
}} }}
/> />
<Cards className="mt-16">
{prevPage ? (
<Card
href={prevPage.url}
className="[&>p]:ml-1 [&>p]:truncate [&>p]:w-full"
description={<>{prevPage.data.description}</>}
title={
<div className="flex items-center gap-1">
<ChevronLeft className="size-4" />
{prevPage.data.title}
</div>
}
/>
) : (
<div></div>
)}
{nextPage ? (
<Card
href={nextPage.url}
description={<>{nextPage.data.description}</>}
title={
<div className="flex items-center gap-1">
{nextPage.data.title}
<ChevronRight className="size-4" />
</div>
}
className="flex flex-col items-end text-right [&>p]:ml-1 [&>p]:truncate [&>p]:w-full"
/>
) : (
<div></div>
)}
</Cards>
</DocsBody> </DocsBody>
</DocsPage> </DocsPage>
); );
} }
export async function generateStaticParams() { export async function generateStaticParams() {
const res = source.getPages().map((page) => ({
slug: page.slugs,
}));
return source.generateParams(); return source.generateParams();
} }
@@ -211,56 +203,3 @@ export async function generateMetadata({
}, },
}; };
} }
function getPageLinks(path: string) {
const current_category_index = contents.findIndex(
(x) => x.list.find((x) => x.href === path)!,
)!;
const current_category = contents[current_category_index];
if (!current_category) return { nextPage: undefined, prevPage: undefined };
// user's current page.
const current_page = current_category.list.find((x) => x.href === path)!;
// the next page in the array.
let next_page = current_category.list.filter((x) => !x.group)[
current_category.list
.filter((x) => !x.group)
.findIndex((x) => x.href === current_page.href) + 1
];
//if there isn't a next page, then go to next cat's page.
if (!next_page) {
// get next cat
let next_category = contents[current_category_index + 1];
// if doesn't exist, return to first cat.
if (!next_category) next_category = contents[0];
next_page = next_category.list[0];
if (next_page.group) {
next_page = next_category.list[1];
}
}
// the prev page in the array.
let prev_page = current_category.list.filter((x) => !x.group)[
current_category.list
.filter((x) => !x.group)
.findIndex((x) => x.href === current_page.href) - 1
];
// if there isn't a prev page, then go to prev cat's page.
if (!prev_page) {
// get prev cat
let prev_category = contents[current_category_index - 1];
// if doesn't exist, return to last cat.
if (!prev_category) prev_category = contents[contents.length - 1];
prev_page = prev_category.list[prev_category.list.length - 1];
if (prev_page.group) {
prev_page = prev_category.list[prev_category.list.length - 2];
}
}
const pages = source.getPages();
let next_page2 = pages.find((x) => x.url === next_page.href);
let prev_page2 = pages.find((x) => x.url === prev_page.href);
if (path === "/docs/introduction") prev_page2 = undefined;
return { nextPage: next_page2, prevPage: prev_page2 };
}

View File

@@ -1,26 +1,7 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs"; import { DocsLayout } from "@/components/docs/docs";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { docsOptions } from "../layout.config"; import { docsOptions } from "../layout.config";
import ArticleLayout from "@/components/side-bar";
import { cn } from "@/lib/utils";
export default function Layout({ children }: { children: ReactNode }) { export default function Layout({ children }: { children: ReactNode }) {
return ( return <DocsLayout {...docsOptions}>{children}</DocsLayout>;
<DocsLayout
{...docsOptions}
sidebar={{
component: (
<div
className={cn(
"[--fd-tocnav-height:36px] md:mr-[268px] lg:mr-[286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px] ",
)}
>
<ArticleLayout />
</div>
),
}}
>
{children}
</DocsLayout>
);
} }

View File

@@ -1,6 +1,7 @@
import { remark } from "remark"; import { remark } from "remark";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { fileGenerator, remarkDocGen, remarkInstall } from "fumadocs-docgen"; import { fileGenerator, remarkDocGen } from "fumadocs-docgen";
import { remarkNpm } from "fumadocs-core/mdx-plugins";
import remarkStringify from "remark-stringify"; import remarkStringify from "remark-stringify";
import remarkMdx from "remark-mdx"; import remarkMdx from "remark-mdx";
import { remarkAutoTypeTable } from "fumadocs-typescript"; import { remarkAutoTypeTable } from "fumadocs-typescript";
@@ -13,7 +14,7 @@ const processor = remark()
.use(remarkGfm) .use(remarkGfm)
.use(remarkAutoTypeTable) .use(remarkAutoTypeTable)
.use(remarkDocGen, { generators: [fileGenerator()] }) .use(remarkDocGen, { generators: [fileGenerator()] })
.use(remarkInstall) .use(remarkNpm)
.use(remarkStringify); .use(remarkStringify);
export async function getLLMText(docPage: any) { export async function getLLMText(docPage: any) {

View File

@@ -4,8 +4,7 @@
@config "../tailwind.config.js"; @config "../tailwind.config.js";
@plugin 'tailwindcss-animate'; @plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
@source '../node_modules/fumadocs-ui/dist/**/*.js';
:root { :root {
--fd-nav-height: 56px; --fd-nav-height: 56px;

View File

@@ -1,24 +1,5 @@
import { changelogs, source } from "@/lib/source"; import { source } from "@/lib/source";
import { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
export const baseOptions: BaseLayoutProps = {
nav: {
enabled: false,
},
links: [
{
text: "Documentation",
url: "/docs",
active: "nested-url",
},
],
};
export const docsOptions = { export const docsOptions = {
...baseOptions,
tree: source.pageTree, tree: source.pageTree,
}; };
export const changelogOptions = {
...baseOptions,
tree: changelogs.pageTree,
};

View File

@@ -1,9 +1,7 @@
import { ApiReference } from "@scalar/nextjs-api-reference"; import { ApiReference } from "@scalar/nextjs-api-reference";
const config = { export const GET = ApiReference({
spec: { spec: {
url: "/openapi.yml", url: "/openapi.yml",
}, },
}; });
export const GET = ApiReference(config);

View File

@@ -0,0 +1,49 @@
"use client";
import { Menu, X } from "lucide-react";
import { type ButtonHTMLAttributes, type HTMLAttributes } from "react";
import { cn } from "../../lib/utils";
import { buttonVariants } from "./ui/button";
import { useSidebar } from "fumadocs-ui/provider";
import { useNav } from "./layout/nav";
import { SidebarTrigger } from "fumadocs-core/sidebar";
export function Navbar(props: HTMLAttributes<HTMLElement>) {
const { open } = useSidebar();
const { isTransparent } = useNav();
return (
<header
id="nd-subnav"
{...props}
className={cn(
"sticky top-(--fd-banner-height) z-30 flex h-14 flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors",
(!isTransparent || open) && "bg-fd-background/80",
props.className,
)}
>
{props.children}
</header>
);
}
export function NavbarSidebarTrigger(
props: ButtonHTMLAttributes<HTMLButtonElement>,
) {
const { open } = useSidebar();
return (
<SidebarTrigger
{...props}
className={cn(
buttonVariants({
color: "ghost",
size: "icon",
}),
props.className,
)}
>
{open ? <X /> : <Menu />}
</SidebarTrigger>
);
}

View File

@@ -0,0 +1,57 @@
import type { PageTree } from "fumadocs-core/server";
import { type ReactNode, type HTMLAttributes } from "react";
import { cn } from "../../lib/utils";
import { type BaseLayoutProps } from "./shared";
import { TreeContextProvider } from "fumadocs-ui/provider";
import { NavProvider } from "./layout/nav";
import { type PageStyles, StylesProvider } from "fumadocs-ui/provider";
import ArticleLayout from "../side-bar";
export interface DocsLayoutProps extends BaseLayoutProps {
tree: PageTree.Root;
containerProps?: HTMLAttributes<HTMLDivElement>;
}
export function DocsLayout({ children, ...props }: DocsLayoutProps): ReactNode {
const variables = cn(
"[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] lg:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]",
);
const pageStyles: PageStyles = {
tocNav: cn("xl:hidden"),
toc: cn("max-xl:hidden"),
};
return (
<TreeContextProvider tree={props.tree}>
<NavProvider>
<main
id="nd-docs-layout"
{...props.containerProps}
className={cn(
"flex flex-1 flex-row pe-(--fd-layout-offset)",
variables,
props.containerProps?.className,
)}
style={
{
"--fd-layout-offset":
"max(calc(50vw - var(--fd-layout-width) / 2), 0px)",
...props.containerProps?.style,
} as object
}
>
<div
className={cn(
"[--fd-tocnav-height:36px] md:mr-[268px] lg:mr-[286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px] ",
)}
>
<ArticleLayout />
</div>
<StylesProvider {...pageStyles}>{children}</StylesProvider>
</main>
</NavProvider>
</TreeContextProvider>
);
}

View File

@@ -0,0 +1,93 @@
"use client";
import Link, { type LinkProps } from "fumadocs-core/link";
import {
createContext,
type ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { cn } from "../../../lib/utils";
import { useI18n } from "fumadocs-ui/provider";
export interface NavProviderProps {
/**
* Use transparent background
*
* @defaultValue none
*/
transparentMode?: "always" | "top" | "none";
}
export interface TitleProps {
title?: ReactNode;
/**
* Redirect url of title
* @defaultValue '/'
*/
url?: string;
}
interface NavContextType {
isTransparent: boolean;
}
const NavContext = createContext<NavContextType>({
isTransparent: false,
});
export function NavProvider({
transparentMode = "none",
children,
}: NavProviderProps & { children: ReactNode }) {
const [transparent, setTransparent] = useState(transparentMode !== "none");
useEffect(() => {
if (transparentMode !== "top") return;
const listener = () => {
setTransparent(window.scrollY < 10);
};
listener();
window.addEventListener("scroll", listener);
return () => {
window.removeEventListener("scroll", listener);
};
}, [transparentMode]);
return (
<NavContext.Provider
value={useMemo(() => ({ isTransparent: transparent }), [transparent])}
>
{children}
</NavContext.Provider>
);
}
export function useNav(): NavContextType {
return useContext(NavContext);
}
export function Title({
title,
url,
...props
}: TitleProps & Omit<LinkProps, "title">) {
const { locale } = useI18n();
return (
<Link
href={url ?? (locale ? `/${locale}` : "/")}
{...props}
className={cn(
"inline-flex items-center gap-2.5 font-semibold",
props.className,
)}
>
{title}
</Link>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { cva } from "class-variance-authority";
import { Moon, Sun, Airplay } from "lucide-react";
import { useTheme } from "next-themes";
import { type HTMLAttributes, useLayoutEffect, useState } from "react";
import { cn } from "../../../lib/utils";
const itemVariants = cva(
"size-6.5 rounded-full p-1.5 text-fd-muted-foreground",
{
variants: {
active: {
true: "bg-fd-accent text-fd-accent-foreground",
false: "text-fd-muted-foreground",
},
},
},
);
const full = [
["light", Sun] as const,
["dark", Moon] as const,
["system", Airplay] as const,
];
export function ThemeToggle({
className,
mode = "light-dark",
...props
}: HTMLAttributes<HTMLElement> & {
mode?: "light-dark" | "light-dark-system";
}) {
const { setTheme, theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useLayoutEffect(() => {
setMounted(true);
}, []);
const container = cn(
"inline-flex items-center rounded-full border p-1",
className,
);
if (mode === "light-dark") {
const value = mounted ? resolvedTheme : null;
return (
<button
className={container}
aria-label={`Toggle Theme`}
onClick={() => setTheme(value === "light" ? "dark" : "light")}
data-theme-toggle=""
{...props}
>
{full.map(([key, Icon]) => {
if (key === "system") return;
return (
<Icon
key={key}
fill="currentColor"
className={cn(itemVariants({ active: value === key }))}
/>
);
})}
</button>
);
}
const value = mounted ? theme : null;
return (
<div className={container} data-theme-toggle="" {...props}>
{full.map(([key, Icon]) => (
<button
key={key}
aria-label={key}
className={cn(itemVariants({ active: value === key }))}
onClick={() => setTheme(key)}
>
<Icon className="size-full" fill="currentColor" />
</button>
))}
</div>
);
}

View File

@@ -0,0 +1,73 @@
import { type HTMLAttributes, type RefObject, useEffect, useRef } from "react";
import * as Primitive from "fumadocs-core/toc";
import { useOnChange } from "fumadocs-core/utils/use-on-change";
import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
export type TOCThumb = [top: number, height: number];
function calc(container: HTMLElement, active: string[]): TOCThumb {
if (active.length === 0 || container.clientHeight === 0) {
return [0, 0];
}
let upper = Number.MAX_VALUE,
lower = 0;
for (const item of active) {
const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`);
if (!element) continue;
const styles = getComputedStyle(element);
upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop));
lower = Math.max(
lower,
element.offsetTop +
element.clientHeight -
parseFloat(styles.paddingBottom),
);
}
return [upper, lower - upper];
}
function update(element: HTMLElement, info: TOCThumb): void {
element.style.setProperty("--fd-top", `${info[0]}px`);
element.style.setProperty("--fd-height", `${info[1]}px`);
}
export function TocThumb({
containerRef,
...props
}: HTMLAttributes<HTMLDivElement> & {
containerRef: RefObject<HTMLElement | null>;
}) {
const active = Primitive.useActiveAnchors();
const thumbRef = useRef<HTMLDivElement>(null);
const onResize = useEffectEvent(() => {
if (!containerRef.current || !thumbRef.current) return;
update(thumbRef.current, calc(containerRef.current, active));
});
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
onResize();
const observer = new ResizeObserver(onResize);
observer.observe(container);
return () => {
observer.disconnect();
};
}, [containerRef, onResize]);
useOnChange(active, () => {
if (!containerRef.current || !thumbRef.current) return;
update(thumbRef.current, calc(containerRef.current, active));
});
return <div ref={thumbRef} role="none" {...props} />;
}

View File

@@ -0,0 +1,345 @@
"use client";
import type { TOCItemType } from "fumadocs-core/server";
import * as Primitive from "fumadocs-core/toc";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
type ReactNode,
use,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
import { useI18n } from "fumadocs-ui/provider";
import { TocThumb } from "./toc-thumb";
import { ScrollArea, ScrollViewport } from "../ui/scroll-area";
import type {
PopoverContentProps,
PopoverTriggerProps,
} from "@radix-ui/react-popover";
import { ChevronRight, Text } from "lucide-react";
import { usePageStyles } from "fumadocs-ui/provider";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../ui/collapsible";
export interface TOCProps {
/**
* Custom content in TOC container, before the main TOC
*/
header?: ReactNode;
/**
* Custom content in TOC container, after the main TOC
*/
footer?: ReactNode;
children: ReactNode;
}
export function Toc(props: HTMLAttributes<HTMLDivElement>) {
const { toc } = usePageStyles();
return (
<div
id="nd-toc"
{...props}
className={cn(
"sticky top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] h-(--fd-toc-height) pb-2 pt-12",
toc,
props.className,
)}
style={
{
...props.style,
"--fd-toc-height":
"calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))",
} as object
}
>
<div className="flex h-full w-(--fd-toc-width) max-w-full flex-col gap-3 pe-4">
{props.children}
</div>
</div>
);
}
export function TocItemsEmpty() {
const { text } = useI18n();
return (
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
{text.tocNoHeadings}
</div>
);
}
export function TOCScrollArea({
isMenu,
...props
}: ComponentProps<typeof ScrollArea> & { isMenu?: boolean }) {
const viewRef = useRef<HTMLDivElement>(null);
return (
<ScrollArea
{...props}
className={cn("flex flex-col ps-px", props.className)}
>
<Primitive.ScrollProvider containerRef={viewRef}>
<ScrollViewport
className={cn(
"relative min-h-0 text-sm",
isMenu && "mt-2 mb-4 mx-4 md:mx-6",
)}
ref={viewRef}
>
{props.children}
</ScrollViewport>
</Primitive.ScrollProvider>
</ScrollArea>
);
}
export function TOCItems({ items }: { items: TOCItemType[] }) {
const containerRef = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState<{
path: string;
width: number;
height: number;
}>();
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
function onResize(): void {
if (container.clientHeight === 0) return;
let w = 0,
h = 0;
const d: string[] = [];
for (let i = 0; i < items.length; i++) {
const element: HTMLElement | null = container.querySelector(
`a[href="#${items[i].url.slice(1)}"]`,
);
if (!element) continue;
const styles = getComputedStyle(element);
const offset = getLineOffset(items[i].depth) + 1,
top = element.offsetTop + parseFloat(styles.paddingTop),
bottom =
element.offsetTop +
element.clientHeight -
parseFloat(styles.paddingBottom);
w = Math.max(offset, w);
h = Math.max(h, bottom);
d.push(`${i === 0 ? "M" : "L"}${offset} ${top}`);
d.push(`L${offset} ${bottom}`);
}
setSvg({
path: d.join(" "),
width: w + 1,
height: h,
});
}
const observer = new ResizeObserver(onResize);
onResize();
observer.observe(container);
return () => {
observer.disconnect();
};
}, [items]);
if (items.length === 0) return <TocItemsEmpty />;
return (
<>
{svg ? (
<div
className="absolute start-0 top-0 rtl:-scale-x-100"
style={{
width: svg.width,
height: svg.height,
maskImage: `url("data:image/svg+xml,${
// Inline SVG
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`,
)
}")`,
}}
>
<TocThumb
containerRef={containerRef}
className="mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all"
/>
</div>
) : null}
<div className="flex flex-col" ref={containerRef}>
{items.map((item, i) => (
<TOCItem
key={item.url}
item={item}
upper={items[i - 1]?.depth}
lower={items[i + 1]?.depth}
/>
))}
</div>
</>
);
}
function getItemOffset(depth: number): number {
if (depth <= 2) return 14;
if (depth === 3) return 26;
return 36;
}
function getLineOffset(depth: number): number {
return depth >= 3 ? 10 : 0;
}
function TOCItem({
item,
upper = item.depth,
lower = item.depth,
}: {
item: TOCItemType;
upper?: number;
lower?: number;
}) {
const offset = getLineOffset(item.depth),
upperOffset = getLineOffset(upper),
lowerOffset = getLineOffset(lower);
return (
<Primitive.TOCItem
href={item.url}
style={{
paddingInlineStart: getItemOffset(item.depth),
}}
className="prose relative py-1.5 text-sm text-fd-muted-foreground transition-colors [overflow-wrap:anywhere] first:pt-0 last:pb-0 data-[active=true]:text-fd-primary"
>
{offset !== upperOffset ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
className="absolute -top-1.5 start-0 size-4 rtl:-scale-x-100"
>
<line
x1={upperOffset}
y1="0"
x2={offset}
y2="12"
className="stroke-fd-foreground/10"
strokeWidth="1"
/>
</svg>
) : null}
<div
className={cn(
"absolute inset-y-0 w-px bg-fd-foreground/10",
offset !== upperOffset && "top-1.5",
offset !== lowerOffset && "bottom-1.5",
)}
style={{
insetInlineStart: offset,
}}
/>
{item.title}
</Primitive.TOCItem>
);
}
type MakeRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
} | null>(null);
const TocProvider = Context.Provider || Context;
export function TocPopover({
open,
onOpenChange,
ref: _ref,
...props
}: MakeRequired<ComponentProps<typeof Collapsible>, "open" | "onOpenChange">) {
return (
<Collapsible open={open} onOpenChange={onOpenChange} {...props}>
<TocProvider
value={useMemo(
() => ({
open,
setOpen: onOpenChange,
}),
[onOpenChange, open],
)}
>
{props.children}
</TocProvider>
</Collapsible>
);
}
export function TocPopoverTrigger({
items,
...props
}: PopoverTriggerProps & { items: TOCItemType[] }) {
const { text } = useI18n();
const { open } = use(Context)!;
const active = Primitive.useActiveAnchor();
const current = useMemo(() => {
return items.find((item) => active === item.url.slice(1))?.title;
}, [items, active]);
return (
<CollapsibleTrigger
{...props}
className={cn(
"inline-flex items-center text-sm gap-2 text-nowrap px-4 py-2.5 text-start md:px-6 focus-visible:outline-none",
props.className,
)}
>
<Text className="size-4 shrink-0" />
{text.toc}
<ChevronRight
className={cn(
"size-4 shrink-0 text-fd-muted-foreground transition-all",
!current && "opacity-0",
open ? "rotate-90" : "-ms-1.5",
)}
/>
<span
className={cn(
"truncate text-fd-muted-foreground transition-opacity -ms-1.5",
(!current || open) && "opacity-0",
)}
>
{current}
</span>
</CollapsibleTrigger>
);
}
export function TocPopoverContent(props: PopoverContentProps) {
return (
<CollapsibleContent
data-toc-popover=""
className="flex flex-col max-h-[50vh]"
{...props}
>
{props.children}
</CollapsibleContent>
);
}

View File

@@ -0,0 +1,254 @@
"use client";
import {
Fragment,
type HTMLAttributes,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import Link from "next/link";
import { cva } from "class-variance-authority";
import { cn } from "../../lib/utils";
import { useI18n } from "fumadocs-ui/provider";
import { useTreeContext, useTreePath } from "fumadocs-ui/provider";
import { useSidebar } from "fumadocs-ui/provider";
import type { PageTree } from "fumadocs-core/server";
import { usePathname } from "next/navigation";
import { useNav } from "./layout/nav";
import {
type BreadcrumbOptions,
getBreadcrumbItemsFromPath,
} from "fumadocs-core/breadcrumb";
import { usePageStyles } from "fumadocs-ui/provider";
import { isActive } from "../../lib/is-active";
import { TocPopover } from "./layout/toc";
import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
export function TocPopoverHeader(props: HTMLAttributes<HTMLDivElement>) {
const ref = useRef<HTMLElement>(null);
const [open, setOpen] = useState(false);
const sidebar = useSidebar();
const { tocNav } = usePageStyles();
const { isTransparent } = useNav();
const onClick = useEffectEvent((e: Event) => {
if (!open) return;
if (ref.current && !ref.current.contains(e.target as HTMLElement))
setOpen(false);
});
useEffect(() => {
window.addEventListener("click", onClick);
return () => {
window.removeEventListener("click", onClick);
};
}, [onClick]);
return (
<div
className={cn("sticky overflow-visible z-10", tocNav, props.className)}
style={{
top: "calc(var(--fd-banner-height) + var(--fd-nav-height))",
}}
>
<TocPopover open={open} onOpenChange={setOpen} asChild>
<header
ref={ref}
id="nd-tocnav"
{...props}
className={cn(
"border-b border-fd-foreground/10 backdrop-blur-md transition-colors",
(!isTransparent || open) && "bg-fd-background/80",
open && "shadow-lg",
sidebar.open && "max-md:hidden",
)}
>
{props.children}
</header>
</TocPopover>
</div>
);
}
export function PageBody(props: HTMLAttributes<HTMLDivElement>) {
const { page } = usePageStyles();
return (
<div
id="nd-page"
{...props}
className={cn("flex w-full min-w-0 flex-col", page, props.className)}
>
{props.children}
</div>
);
}
export function PageArticle(props: HTMLAttributes<HTMLElement>) {
const { article } = usePageStyles();
return (
<article
{...props}
className={cn(
"flex w-full flex-1 flex-col gap-6 px-4 pt-8 md:px-6 md:pt-12 xl:px-12 xl:mx-auto",
article,
props.className,
)}
>
{props.children}
</article>
);
}
export function LastUpdate(props: { date: Date }) {
const { text } = useI18n();
const [date, setDate] = useState("");
useEffect(() => {
// to the timezone of client
setDate(props.date.toLocaleDateString());
}, [props.date]);
return (
<p className="text-sm text-fd-muted-foreground">
{text.lastUpdate} {date}
</p>
);
}
export interface FooterProps {
/**
* Items including information for the next and previous page
*/
items?: {
previous?: { name: string; url: string };
next?: { name: string; url: string };
};
}
const itemVariants = cva(
"flex w-full flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground",
);
const itemLabel = cva(
"inline-flex items-center gap-0.5 text-fd-muted-foreground",
);
function scanNavigationList(tree: PageTree.Node[]) {
const list: PageTree.Item[] = [];
tree.forEach((node) => {
if (node.type === "folder") {
if (node.index) {
list.push(node.index);
}
list.push(...scanNavigationList(node.children));
return;
}
if (node.type === "page" && !node.external) {
list.push(node);
}
});
return list;
}
const listCache = new WeakMap<PageTree.Root, PageTree.Item[]>();
export function Footer({ items }: FooterProps) {
const { root } = useTreeContext();
const { text } = useI18n();
const pathname = usePathname();
const { previous, next } = useMemo(() => {
if (items) return items;
const cached = listCache.get(root);
const list = cached ?? scanNavigationList(root.children);
listCache.set(root, list);
const idx = list.findIndex((item) => isActive(item.url, pathname, false));
if (idx === -1) return {};
return {
previous: list[idx - 1],
next: list[idx + 1],
};
}, [items, pathname, root]);
return (
<div className="grid grid-cols-2 gap-4 pb-6">
{previous ? (
<Link href={previous.url} className={cn(itemVariants())}>
<div className={cn(itemLabel())}>
<ChevronLeft className="-ms-1 size-4 shrink-0 rtl:rotate-180" />
<p>{text.previousPage}</p>
</div>
<p className="font-medium md:text-[15px]">{previous.name}</p>
</Link>
) : null}
{next ? (
<Link
href={next.url}
className={cn(itemVariants({ className: "col-start-2 text-end" }))}
>
<div className={cn(itemLabel({ className: "flex-row-reverse" }))}>
<ChevronRight className="-me-1 size-4 shrink-0 rtl:rotate-180" />
<p>{text.nextPage}</p>
</div>
<p className="font-medium md:text-[15px]">{next.name}</p>
</Link>
) : null}
</div>
);
}
export type BreadcrumbProps = BreadcrumbOptions;
export function Breadcrumb(options: BreadcrumbProps) {
const path = useTreePath();
const { root } = useTreeContext();
const items = useMemo(() => {
return getBreadcrumbItemsFromPath(root, path, {
includePage: options.includePage ?? false,
...options,
});
}, [options, path, root]);
if (items.length === 0) return null;
return (
<div className="flex flex-row items-center gap-1.5 text-[15px] text-fd-muted-foreground">
{items.map((item, i) => {
const className = cn(
"truncate",
i === items.length - 1 && "text-fd-primary font-medium",
);
return (
<Fragment key={i}>
{i !== 0 && <span className="text-fd-foreground/30">/</span>}
{item.url ? (
<Link
href={item.url}
className={cn(className, "transition-opacity hover:opacity-80")}
>
{item.name}
</Link>
) : (
<span className={className}>{item.name}</span>
)}
</Fragment>
);
})}
</div>
);
}

View File

@@ -0,0 +1,298 @@
import type { TableOfContents } from "fumadocs-core/server";
import {
type AnchorHTMLAttributes,
forwardRef,
type HTMLAttributes,
type ReactNode,
} from "react";
import { type AnchorProviderProps, AnchorProvider } from "fumadocs-core/toc";
import { replaceOrDefault } from "./shared";
import { cn } from "../../lib/utils";
import {
Footer,
type FooterProps,
LastUpdate,
TocPopoverHeader,
type BreadcrumbProps,
PageBody,
PageArticle,
} from "./page.client";
import {
Toc,
TOCItems,
TocPopoverTrigger,
TocPopoverContent,
type TOCProps,
TOCScrollArea,
} from "./layout/toc";
import { buttonVariants } from "./ui/button";
import { Edit, Text } from "lucide-react";
import { I18nLabel } from "fumadocs-ui/provider";
type TableOfContentOptions = Omit<TOCProps, "items" | "children"> &
Pick<AnchorProviderProps, "single"> & {
enabled: boolean;
component: ReactNode;
};
type TableOfContentPopoverOptions = Omit<TableOfContentOptions, "single">;
interface EditOnGitHubOptions
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "children"> {
owner: string;
repo: string;
/**
* SHA or ref (branch or tag) name.
*
* @defaultValue main
*/
sha?: string;
/**
* File path in the repo
*/
path: string;
}
interface BreadcrumbOptions extends BreadcrumbProps {
enabled: boolean;
component: ReactNode;
/**
* Show the full path to the current page
*
* @defaultValue false
* @deprecated use `includePage` instead
*/
full?: boolean;
}
interface FooterOptions extends FooterProps {
enabled: boolean;
component: ReactNode;
}
export interface DocsPageProps {
toc?: TableOfContents;
/**
* Extend the page to fill all available space
*
* @defaultValue false
*/
full?: boolean;
tableOfContent?: Partial<TableOfContentOptions>;
tableOfContentPopover?: Partial<TableOfContentPopoverOptions>;
/**
* Replace or disable breadcrumb
*/
breadcrumb?: Partial<BreadcrumbOptions>;
/**
* Footer navigation, you can disable it by passing `false`
*/
footer?: Partial<FooterOptions>;
editOnGithub?: EditOnGitHubOptions;
lastUpdate?: Date | string | number;
container?: HTMLAttributes<HTMLDivElement>;
article?: HTMLAttributes<HTMLElement>;
children: ReactNode;
}
export function DocsPage({
toc = [],
full = false,
tableOfContentPopover: {
enabled: tocPopoverEnabled,
component: tocPopoverReplace,
...tocPopoverOptions
} = {},
tableOfContent: {
enabled: tocEnabled,
component: tocReplace,
...tocOptions
} = {},
...props
}: DocsPageProps) {
const isTocRequired =
toc.length > 0 ||
tocOptions.footer !== undefined ||
tocOptions.header !== undefined;
// disable TOC on full mode, you can still enable it with `enabled` option.
tocEnabled ??= !full && isTocRequired;
tocPopoverEnabled ??=
toc.length > 0 ||
tocPopoverOptions.header !== undefined ||
tocPopoverOptions.footer !== undefined;
return (
<AnchorProvider toc={toc} single={tocOptions.single}>
<PageBody
{...props.container}
className={cn(props.container?.className)}
style={
{
"--fd-tocnav-height": !tocPopoverEnabled ? "0px" : undefined,
...props.container?.style,
} as object
}
>
{replaceOrDefault(
{ enabled: tocPopoverEnabled, component: tocPopoverReplace },
<TocPopoverHeader className="h-10">
<TocPopoverTrigger className="w-full" items={toc} />
<TocPopoverContent>
{tocPopoverOptions.header}
<TOCScrollArea isMenu>
<TOCItems items={toc} />
</TOCScrollArea>
{tocPopoverOptions.footer}
</TocPopoverContent>
</TocPopoverHeader>,
{
items: toc,
...tocPopoverOptions,
},
)}
<PageArticle
{...props.article}
className={cn(
full || !tocEnabled ? "max-w-[1120px]" : "max-w-[860px]",
props.article?.className,
)}
>
{props.children}
<div role="none" className="flex-1" />
<div className="flex flex-row flex-wrap items-center justify-between gap-4 empty:hidden">
{props.editOnGithub ? (
<EditOnGitHub {...props.editOnGithub} />
) : null}
{props.lastUpdate ? (
<LastUpdate date={new Date(props.lastUpdate)} />
) : null}
</div>
{replaceOrDefault(
props.footer,
<Footer items={props.footer?.items} />,
)}
</PageArticle>
</PageBody>
{replaceOrDefault(
{ enabled: tocEnabled, component: tocReplace },
<Toc>
{tocOptions.header}
<h3 className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground">
<Text className="size-4" />
<I18nLabel label="toc" />
</h3>
<TOCScrollArea>
<TOCItems items={toc} />
</TOCScrollArea>
{tocOptions.footer}
</Toc>,
{
items: toc,
...tocOptions,
},
)}
</AnchorProvider>
);
}
function EditOnGitHub({
owner,
repo,
sha,
path,
...props
}: EditOnGitHubOptions) {
const href = `https://github.com/${owner}/${repo}/blob/${sha}/${path.startsWith("/") ? path.slice(1) : path}`;
return (
<a
href={href}
target="_blank"
rel="noreferrer noopener"
{...props}
className={cn(
buttonVariants({
color: "secondary",
className: "gap-1.5 text-fd-muted-foreground",
}),
props.className,
)}
>
<Edit className="size-3.5" />
<I18nLabel label="editOnGithub" />
</a>
);
}
/**
* Add typography styles
*/
export const DocsBody = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>((props, ref) => (
<div ref={ref} {...props} className={cn("prose", props.className)}>
{props.children}
</div>
));
DocsBody.displayName = "DocsBody";
export const DocsDescription = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
>((props, ref) => {
// don't render if no description provided
if (props.children === undefined) return null;
return (
<p
ref={ref}
{...props}
className={cn("mb-8 text-lg text-fd-muted-foreground", props.className)}
>
{props.children}
</p>
);
});
DocsDescription.displayName = "DocsDescription";
export const DocsTitle = forwardRef<
HTMLHeadingElement,
HTMLAttributes<HTMLHeadingElement>
>((props, ref) => {
return (
<h1
ref={ref}
{...props}
className={cn("text-3xl font-semibold", props.className)}
>
{props.children}
</h1>
);
});
DocsTitle.displayName = "DocsTitle";
/**
* For separate MDX page
*/
export function withArticle({ children }: { children: ReactNode }): ReactNode {
return (
<main className="container py-12">
<article className="prose">{children}</article>
</main>
);
}

View File

@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import { Slot } from "@radix-ui/react-slot";
export interface BaseLayoutProps {
children?: ReactNode;
}
export function replaceOrDefault(
obj:
| {
enabled?: boolean;
component?: ReactNode;
}
| undefined,
def: ReactNode,
customComponentProps?: object,
disabled?: ReactNode,
): ReactNode {
if (obj?.enabled === false) return disabled;
if (obj?.component !== undefined)
return <Slot {...customComponentProps}>{obj.component}</Slot>;
return def;
}

View File

@@ -0,0 +1,22 @@
import { cva } from "class-variance-authority";
export const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
color: {
primary:
"bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80",
outline: "border hover:bg-fd-accent hover:text-fd-accent-foreground",
ghost: "hover:bg-fd-accent hover:text-fd-accent-foreground",
secondary:
"border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground",
},
size: {
sm: "gap-1 p-0.5 text-xs",
icon: "p-1.5 [&_svg]:size-5",
"icon-sm": "p-1.5 [&_svg]:size-4.5",
},
},
},
);

View File

@@ -0,0 +1,39 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
import { forwardRef, useEffect, useState } from "react";
import { cn } from "../../../lib/utils";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
>(({ children, ...props }, ref) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<CollapsiblePrimitive.CollapsibleContent
ref={ref}
{...props}
className={cn(
"overflow-hidden",
mounted &&
"data-[state=closed]:animate-fd-collapsible-up data-[state=open]:animate-fd-collapsible-down",
props.className,
)}
>
{children}
</CollapsiblePrimitive.CollapsibleContent>
);
});
CollapsibleContent.displayName =
CollapsiblePrimitive.CollapsibleContent.displayName;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -0,0 +1,32 @@
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "../../../lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
side="bottom"
className={cn(
"z-50 min-w-[220px] max-w-[98vw] rounded-lg border bg-fd-popover p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[state=closed]:animate-fd-popover-out data-[state=open]:animate-fd-popover-in",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
const PopoverClose = PopoverPrimitive.PopoverClose;
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };

View File

@@ -0,0 +1,57 @@
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as React from "react";
import { cn } from "../../../lib/utils";
const ScrollArea = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("overflow-hidden", className)}
{...props}
>
{children}
<ScrollAreaPrimitive.Corner />
<ScrollBar orientation="vertical" />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollViewport = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn("size-full rounded-[inherit]", className)}
{...props}
>
{children}
</ScrollAreaPrimitive.Viewport>
));
ScrollViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
const ScrollBar = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.Scrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex select-none data-[state=hidden]:animate-fd-fade-out",
orientation === "vertical" && "h-full w-1.5",
orientation === "horizontal" && "h-1.5 flex-col",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-fd-border" />
</ScrollAreaPrimitive.Scrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName;
export { ScrollArea, ScrollBar, ScrollViewport };

View File

@@ -19,6 +19,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { ReactNode, SVGProps } from "react"; import { ReactNode, SVGProps } from "react";
import { Icons } from "./icons"; import { Icons } from "./icons";
import { PageTree } from "fumadocs-core/server";
interface Content { interface Content {
title: string; title: string;
@@ -34,6 +35,51 @@ interface Content {
}[]; }[];
} }
export function getPageTree(): PageTree.Root {
return {
$id: "root",
name: "docs",
children: [
{
type: "folder",
root: true,
name: "Docs",
description: "get started, concepts, and plugins.",
children: contents.map(contentToPageTree),
},
{
type: "folder",
root: true,
name: "Examples",
description: "exmaples and guides.",
children: examples.map(contentToPageTree),
},
],
};
}
function contentToPageTree(content: Content): PageTree.Folder {
return {
type: "folder",
icon: <content.Icon />,
name: content.title,
index: content.href
? {
icon: <content.Icon />,
name: content.title,
type: "page",
url: content.href,
}
: undefined,
children: content.list.map((item) => ({
type: "page",
url: item.href,
name: item.title,
icon: <item.icon />,
})),
};
}
export const contents: Content[] = [ export const contents: Content[] = [
{ {
title: "Get Started", title: "Get Started",

View File

@@ -0,0 +1,73 @@
import { CircleCheck, CircleX, Info, TriangleAlert } from "lucide-react";
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { cva } from "class-variance-authority";
type CalloutProps = Omit<
HTMLAttributes<HTMLDivElement>,
"title" | "type" | "icon"
> & {
title?: ReactNode;
/**
* @defaultValue info
*/
type?: "info" | "warn" | "error" | "success" | "warning";
/**
* Force an icon
*/
icon?: ReactNode;
};
const calloutVariants = cva(
"my-4 flex gap-2 rounded-lg border border-s-2 bg-fd-card p-3 text-sm text-fd-card-foreground shadow-md border-dashed rounded-none",
{
variants: {
type: {
info: "border-s-blue-500/50",
warn: "border-s-orange-500/50",
error: "border-s-red-500/50",
success: "border-s-green-500/50",
},
},
},
);
export const Callout = forwardRef<HTMLDivElement, CalloutProps>(
({ className, children, title, type = "info", icon, ...props }, ref) => {
if (type === "warning") type = "warn";
return (
<div
ref={ref}
className={cn(
calloutVariants({
type: type,
}),
className,
)}
{...props}
>
{icon ??
{
info: <Info className="size-5 fill-blue-500 text-fd-card" />,
warn: (
<TriangleAlert className="size-5 fill-orange-500 text-fd-card" />
),
error: <CircleX className="size-5 fill-red-500 text-fd-card" />,
success: (
<CircleCheck className="size-5 fill-green-500 text-fd-card" />
),
}[type]}
<div className="min-w-0 flex flex-col gap-2 flex-1">
{title ? <p className="font-medium !my-0">{title}</p> : null}
<div className="text-fd-muted-foreground prose-no-margin empty:hidden">
{children}
</div>
</div>
</div>
);
},
);
Callout.displayName = "Callout";

View File

@@ -0,0 +1,354 @@
"use client";
import { Check, Copy } from "lucide-react";
import {
ButtonHTMLAttributes,
type ComponentProps,
createContext,
forwardRef,
type HTMLAttributes,
ReactElement,
type ReactNode,
type RefObject,
useCallback,
useContext,
useMemo,
useRef,
} from "react";
import { cn } from "@/lib/utils";
import { useCopyButton } from "./use-copy-button";
import { buttonVariants } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { mergeRefs } from "@/lib/utils";
import { ScrollArea, ScrollBar, ScrollViewport } from "./scroll-area";
export interface CodeBlockProps extends ComponentProps<"figure"> {
/**
* Icon of code block
*
* When passed as a string, it assumes the value is the HTML of icon
*/
icon?: ReactNode;
/**
* Allow to copy code with copy button
*
* @defaultValue true
*/
allowCopy?: boolean;
/**
* Keep original background color generated by Shiki or Rehype Code
*
* @defaultValue false
*/
keepBackground?: boolean;
viewportProps?: HTMLAttributes<HTMLElement>;
/**
* show line numbers
*/
"data-line-numbers"?: boolean;
/**
* @defaultValue 1
*/
"data-line-numbers-start"?: number;
Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode;
}
const TabsContext = createContext<{
containerRef: RefObject<HTMLDivElement | null>;
nested: boolean;
} | null>(null);
export function Pre(props: ComponentProps<"pre">) {
return (
<pre
{...props}
className={cn("min-w-full w-max *:flex *:flex-col", props.className)}
>
{props.children}
</pre>
);
}
export function CodeBlock({
ref,
title,
allowCopy,
keepBackground = false,
icon,
viewportProps = {},
children,
Actions = (props) => (
<div {...props} className={cn("empty:hidden", props.className)} />
),
...props
}: CodeBlockProps) {
const isTab = useContext(TabsContext) !== null;
const areaRef = useRef<HTMLDivElement>(null);
allowCopy ??= !isTab;
const bg = cn(
"bg-fd-secondary",
keepBackground && "bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)",
);
const onCopy = useCallback(() => {
const pre = areaRef.current?.getElementsByTagName("pre").item(0);
if (!pre) return;
const clone = pre.cloneNode(true) as HTMLElement;
clone.querySelectorAll(".nd-copy-ignore").forEach((node) => {
node.remove();
});
void navigator.clipboard.writeText(clone.textContent ?? "");
}, []);
return (
<figure
ref={ref}
dir="ltr"
{...props}
className={cn(
isTab ? [bg, "rounded-lg"] : "my-4 rounded-lg bg-fd-card",
"group shiki relative border shadow-sm outline-none not-prose overflow-hidden text-sm",
props.className,
)}
>
{title ? (
<div
className={cn(
"group flex text-fd-muted-foreground items-center gap-2 ps-3 h-9.5 pr-1 bg-fd-muted",
isTab && "border-b",
)}
>
{typeof icon === "string" ? (
<div
className="[&_svg]:size-3.5"
dangerouslySetInnerHTML={{
__html: icon,
}}
/>
) : (
icon
)}
<figcaption className="flex-1 truncate">{title}</figcaption>
{Actions({
children: allowCopy && <CopyButton onCopy={onCopy} />,
})}
</div>
) : (
Actions({
className: "absolute top-1 right-1 z-2 text-fd-muted-foreground",
children: allowCopy && <CopyButton onCopy={onCopy} />,
})
)}
<div
ref={areaRef}
{...viewportProps}
className={cn(
!isTab && [bg, "rounded-none border border-x-0 border-b-0"],
"text-[13px] overflow-auto max-h-[600px] bg-fd-muted/50 fd-scroll-container",
viewportProps.className,
)}
style={
{
// space for toolbar
"--padding-right": !title ? "calc(var(--spacing) * 8)" : undefined,
counterSet: props["data-line-numbers"]
? `line ${Number(props["data-line-numbers-start"] ?? 1) - 1}`
: undefined,
...viewportProps.style,
} as object
}
>
{children}
</div>
</figure>
);
}
function CopyButton({
className,
onCopy,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
onCopy: () => void;
}): ReactElement {
const [checked, onClick] = useCopyButton(onCopy);
return (
<button
type="button"
className={cn(
buttonVariants({
variant: "ghost",
size: "icon",
}),
"transition-opacity border-none group-hover:opacity-100",
"opacity-0 group-hover:opacity-100",
"group-hover:opacity-100",
className,
)}
aria-label="Copy Text"
onClick={onClick}
{...props}
>
<Check
className={cn("size-3.5 transition-transform", !checked && "scale-0")}
/>
<Copy
className={cn(
"absolute size-3.5 transition-transform",
checked && "scale-0",
)}
/>
</button>
);
}
export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) {
const containerRef = useRef<HTMLDivElement>(null);
const nested = useContext(TabsContext) !== null;
return (
<Tabs
ref={mergeRefs(containerRef, ref)}
{...props}
className={cn(
"bg-fd-card p-1 rounded-xl border overflow-hidden",
!nested && "my-4",
props.className,
)}
>
<TabsContext.Provider
value={useMemo(
() => ({
containerRef,
nested,
}),
[nested],
)}
>
{props.children}
</TabsContext.Provider>
</Tabs>
);
}
export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) {
const { containerRef, nested } = useContext(TabsContext)!;
return (
<TabsList
{...props}
className={cn(
"flex flex-row overflow-x-auto px-1 -mx-1 text-fd-muted-foreground",
props.className,
)}
>
{props.children}
</TabsList>
);
}
export function CodeBlockTabsTrigger({
children,
...props
}: ComponentProps<typeof TabsTrigger>) {
return (
<TabsTrigger
{...props}
className={cn(
"relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 first:ms-1 py-1.5 hover:text-fd-accent-foreground data-[state=active]:text-fd-primary [&_svg]:size-3.5",
props.className,
)}
>
<div className="absolute inset-x-2 bottom-0 h-px group-data-[state=active]:bg-fd-primary" />
{children}
</TabsTrigger>
);
}
// TODO: currently Vite RSC plugin has problem with adding `asChild` here, maybe revisit this in future
export const CodeBlockTab = TabsContent;
export const CodeBlockOld = forwardRef<HTMLElement, CodeBlockProps>(
(
{
title,
allowCopy = true,
keepBackground = false,
icon,
viewportProps,
...props
},
ref,
) => {
const areaRef = useRef<HTMLDivElement>(null);
const onCopy = useCallback(() => {
const pre = areaRef.current?.getElementsByTagName("pre").item(0);
if (!pre) return;
const clone = pre.cloneNode(true) as HTMLElement;
clone.querySelectorAll(".nd-copy-ignore").forEach((node) => {
node.remove();
});
void navigator.clipboard.writeText(clone.textContent ?? "");
}, []);
return (
<figure
ref={ref}
{...props}
className={cn(
"not-prose group fd-codeblock relative my-6 overflow-hidden rounded-lg border bg-fd-secondary/50 text-sm",
keepBackground &&
"bg-[var(--shiki-light-bg)] dark:bg-[var(--shiki-dark-bg)]",
props.className,
)}
>
{title ? (
<div className="flex flex-row items-center gap-2 border-b bg-fd-muted px-4 py-1.5">
{icon ? (
<div
className="text-fd-muted-foreground [&_svg]:size-3.5"
dangerouslySetInnerHTML={
typeof icon === "string"
? {
__html: icon,
}
: undefined
}
>
{typeof icon !== "string" ? icon : null}
</div>
) : null}
<figcaption className="flex-1 truncate text-fd-muted-foreground">
{title}
</figcaption>
{allowCopy ? (
<CopyButton className="-me-2" onCopy={onCopy} />
) : null}
</div>
) : (
allowCopy && (
<CopyButton
className="absolute right-2 top-2 z-[2] backdrop-blur-md"
onCopy={onCopy}
/>
)
)}
<ScrollArea ref={areaRef} dir="ltr">
<ScrollViewport
{...viewportProps}
className={cn("max-h-[600px]", viewportProps?.className)}
>
{props.children}
</ScrollViewport>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</figure>
);
},
);
CodeBlockOld.displayName = "CodeBlockOld";

View File

@@ -27,6 +27,20 @@ function ScrollArea({
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
); );
} }
const ScrollViewport = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={cn("size-full rounded-[inherit]", className)}
{...props}
>
{children}
</ScrollAreaPrimitive.Viewport>
));
ScrollViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
function ScrollBar({ function ScrollBar({
className, className,
@@ -55,4 +69,4 @@ function ScrollBar({
); );
} }
export { ScrollArea, ScrollBar }; export { ScrollArea, ScrollBar, ScrollViewport };

View File

@@ -0,0 +1,31 @@
"use client";
import { type MouseEventHandler, useEffect, useRef, useState } from "react";
import { useEffectEvent } from "fumadocs-core/utils/use-effect-event";
export function useCopyButton(
onCopy: () => void | Promise<void>,
): [checked: boolean, onClick: MouseEventHandler] {
const [checked, setChecked] = useState(false);
const timeoutRef = useRef<number | null>(null);
const onClick: MouseEventHandler = useEffectEvent(() => {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
const res = Promise.resolve(onCopy());
void res.then(() => {
setChecked(true);
timeoutRef.current = window.setTimeout(() => {
setChecked(false);
}, 1500);
});
});
// Avoid updates after being unmounted
useEffect(() => {
return () => {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
};
}, []);
return [checked, onClick];
}

10
docs/lib/is-active.ts Normal file
View File

@@ -0,0 +1,10 @@
export function isActive(
url: string,
pathname: string,
nested = true,
): boolean {
if (url.endsWith("/")) url = url.slice(0, -1);
if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
return url === pathname || (nested && pathname.startsWith(`${url}/`));
}

View File

@@ -1,12 +1,15 @@
import { changelogCollection, docs, blogCollection } from "@/.source"; import { changelogCollection, docs, blogCollection } from "@/.source";
import { getPageTree } from "@/components/sidebar-content";
import { loader } from "fumadocs-core/source"; import { loader } from "fumadocs-core/source";
import { createMDXSource } from "fumadocs-mdx"; import { createMDXSource } from "fumadocs-mdx";
export const source = loader({ export let source = loader({
baseUrl: "/docs", baseUrl: "/docs",
source: docs.toFumadocsSource(), source: docs.toFumadocsSource(),
}); });
source = { ...source, pageTree: getPageTree() };
export const changelogs = loader({ export const changelogs = loader({
baseUrl: "/changelogs", baseUrl: "/changelogs",
source: createMDXSource(changelogCollection), source: createMDXSource(changelogCollection),

View File

@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import type * as React from "react";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -32,3 +33,17 @@ export function formatDate(date: Date) {
.toLocaleDateString("en-US", { month: "short", day: "numeric" }) .toLocaleDateString("en-US", { month: "short", day: "numeric" })
.replace(",", ""); .replace(",", "");
} }
export function mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref) {
ref.current = value;
}
});
};
}

View File

@@ -5,70 +5,71 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev --turbopack",
"start": "next start", "start": "next start",
"postinstall": "fumadocs-mdx", "postinstall": "fumadocs-mdx",
"scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts" "scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.6", "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.6", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.2.8",
"@scalar/nextjs-api-reference": "^0.5.15", "@scalar/nextjs-api-reference": "^0.5.15",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0", "cmdk": "1.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.1", "embla-carousel-react": "^8.5.1",
"fumadocs-core": "15.0.15", "fumadocs-core": "15.7.1",
"fumadocs-docgen": "2.1.0", "fumadocs-docgen": "2.1.0",
"fumadocs-mdx": "11.5.6", "fumadocs-mdx": "11.8.0",
"fumadocs-typescript": "^4.0.6", "fumadocs-typescript": "^4.0.6",
"fumadocs-ui": "15.0.15", "fumadocs-ui": "15.7.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"input-otp": "^1.4.1", "input-otp": "^1.4.1",
"jotai": "^2.12.1", "jotai": "^2.13.1",
"js-beautify": "^1.15.4",
"jsrsasign": "^11.1.0", "jsrsasign": "^11.1.0",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"motion": "^12.4.10", "motion": "^12.23.12",
"next": "15.5.0", "next": "15.5.0",
"next-themes": "^0.3.0", "next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-day-picker": "8.10.1", "react-day-picker": "8.10.1",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.0", "react-hook-form": "^7.62.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"recharts": "^2.14.1", "recharts": "^2.14.1",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
"sonner": "^2.0.1", "sonner": "^2.0.7",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.2" "zod": "^3.24.2"

View File

@@ -4,8 +4,8 @@ import {
defineCollections, defineCollections,
} from "fumadocs-mdx/config"; } from "fumadocs-mdx/config";
import { z } from "zod"; import { z } from "zod";
import { remarkInstall } from "fumadocs-docgen";
import { remarkAutoTypeTable, createGenerator } from "fumadocs-typescript"; import { remarkAutoTypeTable, createGenerator } from "fumadocs-typescript";
import { remarkNpm } from "fumadocs-core/mdx-plugins";
export const docs = defineDocs({ export const docs = defineDocs({
dir: "./content/docs", dir: "./content/docs",
@@ -44,7 +44,7 @@ export default defineConfig({
mdxOptions: { mdxOptions: {
remarkPlugins: [ remarkPlugins: [
[ [
remarkInstall, remarkNpm,
{ {
persist: { persist: {
id: "persist-install", id: "persist-install",

8747
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff