mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-08 04:19:25 +00:00
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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
49
docs/components/docs/docs.client.tsx
Normal file
49
docs/components/docs/docs.client.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
docs/components/docs/docs.tsx
Normal file
57
docs/components/docs/docs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
docs/components/docs/layout/nav.tsx
Normal file
93
docs/components/docs/layout/nav.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
docs/components/docs/layout/theme-toggle.tsx
Normal file
87
docs/components/docs/layout/theme-toggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
docs/components/docs/layout/toc-thumb.tsx
Normal file
73
docs/components/docs/layout/toc-thumb.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
345
docs/components/docs/layout/toc.tsx
Normal file
345
docs/components/docs/layout/toc.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
254
docs/components/docs/page.client.tsx
Normal file
254
docs/components/docs/page.client.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
298
docs/components/docs/page.tsx
Normal file
298
docs/components/docs/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
docs/components/docs/shared.tsx
Normal file
24
docs/components/docs/shared.tsx
Normal 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;
|
||||||
|
}
|
||||||
22
docs/components/docs/ui/button.tsx
Normal file
22
docs/components/docs/ui/button.tsx
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
39
docs/components/docs/ui/collapsible.tsx
Normal file
39
docs/components/docs/ui/collapsible.tsx
Normal 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 };
|
||||||
32
docs/components/docs/ui/popover.tsx
Normal file
32
docs/components/docs/ui/popover.tsx
Normal 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 };
|
||||||
57
docs/components/docs/ui/scroll-area.tsx
Normal file
57
docs/components/docs/ui/scroll-area.tsx
Normal 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 };
|
||||||
@@ -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",
|
||||||
|
|||||||
73
docs/components/ui/callout.tsx
Normal file
73
docs/components/ui/callout.tsx
Normal 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";
|
||||||
354
docs/components/ui/code-block.tsx
Normal file
354
docs/components/ui/code-block.tsx
Normal 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";
|
||||||
@@ -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 };
|
||||||
|
|||||||
31
docs/components/ui/use-copy-button.tsx
Normal file
31
docs/components/ui/use-copy-button.tsx
Normal 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
10
docs/lib/is-active.ts
Normal 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}/`));
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
8747
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user