mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 04:19:26 +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 { Accordion, Accordions } from "fumadocs-ui/components/accordion";
|
||||
import { Pre } from "fumadocs-ui/components/codeblock";
|
||||
import { DocsBody } from "fumadocs-ui/page";
|
||||
import { Glow } from "../_components/default-changelog";
|
||||
import { IconLink } from "../_components/changelog-layout";
|
||||
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 Image from "next/image";
|
||||
import { BlogPage } from "../_components/blog-list";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
|
||||
const metaTitle = "Blogs";
|
||||
const metaDescription = "Latest changes , fixes and updates.";
|
||||
@@ -119,7 +119,7 @@ export default async function Page({
|
||||
</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="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
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
@@ -151,9 +151,22 @@ export default async function Page({
|
||||
DatabaseTable,
|
||||
Accordion,
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -15,12 +15,12 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import { File, Folder, Files } from "fumadocs-ui/components/files";
|
||||
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
|
||||
import { Pre } from "fumadocs-ui/components/codeblock";
|
||||
import { DocsBody } from "fumadocs-ui/page";
|
||||
import ChangelogPage, { Glow } from "../_components/default-changelog";
|
||||
import { IconLink } from "../_components/changelog-layout";
|
||||
import { XIcon } from "../_components/icons";
|
||||
import { StarField } from "../_components/stat-field";
|
||||
import { GridPatterns } from "../_components/grid-pattern";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
|
||||
const metaTitle = "Changelogs";
|
||||
const metaDescription = "Latest changes , fixes and updates.";
|
||||
@@ -71,7 +71,7 @@ export default async function Page({
|
||||
</div>
|
||||
<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>
|
||||
<DocsBody className="pt-8 md:pt-0">
|
||||
<div className="prose pt-8 md:pt-0">
|
||||
<MDX
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
@@ -103,9 +103,22 @@ export default async function Page({
|
||||
DatabaseTable,
|
||||
Accordion,
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { absoluteUrl } from "@/lib/utils";
|
||||
import DatabaseTable from "@/components/mdx/database-tables";
|
||||
@@ -13,18 +13,22 @@ import { Features } from "@/components/blocks/features";
|
||||
import { ForkButton } from "@/components/fork-button";
|
||||
import Link from "next/link";
|
||||
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 { AutoTypeTable } from "fumadocs-typescript/ui";
|
||||
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 { DividerText } from "@/components/divider-text";
|
||||
import { APIMethod } from "@/components/api-method";
|
||||
import { LLMCopyButton, ViewOptions } from "./page.client";
|
||||
import { GenerateAppleJwt } from "@/components/generate-apple-jwt";
|
||||
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
@@ -37,8 +41,6 @@ export default async function Page({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { nextPage, prevPage } = getPageLinks(page.url);
|
||||
|
||||
const MDX = page.data.body;
|
||||
const avoidLLMHeader = ["Introduction", "Comparison"];
|
||||
return (
|
||||
@@ -49,16 +51,11 @@ export default async function Page({
|
||||
owner: "better-auth",
|
||||
repo: "better-auth",
|
||||
sha: "main",
|
||||
path: `/docs/content/docs/${page.file.path}`,
|
||||
path: `/docs/content/docs/${page.path}`,
|
||||
}}
|
||||
tableOfContent={{
|
||||
style: "clerk",
|
||||
header: <div className="w-10 h-4"></div>,
|
||||
}}
|
||||
footer={{
|
||||
enabled: true,
|
||||
component: <div className="w-10 h-4" />,
|
||||
}}
|
||||
>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
{!avoidLLMHeader.includes(page.data.title) && (
|
||||
@@ -74,6 +71,38 @@ export default async function Page({
|
||||
<MDX
|
||||
components={{
|
||||
...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: ({
|
||||
className,
|
||||
...props
|
||||
@@ -105,19 +134,18 @@ export default async function Page({
|
||||
Accordions,
|
||||
Endpoint,
|
||||
APIMethod,
|
||||
Callout: ({ children, ...props }) => (
|
||||
<defaultMdxComponents.Callout
|
||||
{...props}
|
||||
className={cn(
|
||||
props,
|
||||
"bg-none rounded-none border-dashed border-border",
|
||||
props.type === "info" && "border-l-blue-500/50",
|
||||
props.type === "warn" && "border-l-amber-700/50",
|
||||
props.type === "error" && "border-l-red-500/50",
|
||||
)}
|
||||
>
|
||||
Callout: ({
|
||||
children,
|
||||
type,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
type?: "info" | "warn" | "error" | "success" | "warning";
|
||||
[key: string]: any;
|
||||
}) => (
|
||||
<Callout type={type} {...props}>
|
||||
{children}
|
||||
</defaultMdxComponents.Callout>
|
||||
</Callout>
|
||||
),
|
||||
DividerText,
|
||||
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>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const res = source.getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
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 { docsOptions } from "../layout.config";
|
||||
import ArticleLayout from "@/components/side-bar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <DocsLayout {...docsOptions}>{children}</DocsLayout>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { remark } from "remark";
|
||||
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 remarkMdx from "remark-mdx";
|
||||
import { remarkAutoTypeTable } from "fumadocs-typescript";
|
||||
@@ -13,7 +14,7 @@ const processor = remark()
|
||||
.use(remarkGfm)
|
||||
.use(remarkAutoTypeTable)
|
||||
.use(remarkDocGen, { generators: [fileGenerator()] })
|
||||
.use(remarkInstall)
|
||||
.use(remarkNpm)
|
||||
.use(remarkStringify);
|
||||
|
||||
export async function getLLMText(docPage: any) {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
@config "../tailwind.config.js";
|
||||
@plugin 'tailwindcss-animate';
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
|
||||
:root {
|
||||
--fd-nav-height: 56px;
|
||||
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
import { changelogs, 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
import { source } from "@/lib/source";
|
||||
|
||||
export const docsOptions = {
|
||||
...baseOptions,
|
||||
tree: source.pageTree,
|
||||
};
|
||||
export const changelogOptions = {
|
||||
...baseOptions,
|
||||
tree: changelogs.pageTree,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ApiReference } from "@scalar/nextjs-api-reference";
|
||||
|
||||
const config = {
|
||||
export const GET = ApiReference({
|
||||
spec: {
|
||||
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";
|
||||
import { ReactNode, SVGProps } from "react";
|
||||
import { Icons } from "./icons";
|
||||
import { PageTree } from "fumadocs-core/server";
|
||||
|
||||
interface Content {
|
||||
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[] = [
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
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({
|
||||
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 { getPageTree } from "@/components/sidebar-content";
|
||||
import { loader } from "fumadocs-core/source";
|
||||
import { createMDXSource } from "fumadocs-mdx";
|
||||
|
||||
export const source = loader({
|
||||
export let source = loader({
|
||||
baseUrl: "/docs",
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
|
||||
source = { ...source, pageTree: getPageTree() };
|
||||
|
||||
export const changelogs = loader({
|
||||
baseUrl: "/changelogs",
|
||||
source: createMDXSource(changelogCollection),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import type * as React from "react";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -32,3 +33,17 @@ export function formatDate(date: Date) {
|
||||
.toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
||||
.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",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbopack",
|
||||
"start": "next start",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"scripts:endpoint-to-doc": "bun ./scripts/endpoint-to-doc/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@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-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@scalar/nextjs-api-reference": "^0.5.15",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"fumadocs-core": "15.0.15",
|
||||
"fumadocs-core": "15.7.1",
|
||||
"fumadocs-docgen": "2.1.0",
|
||||
"fumadocs-mdx": "11.5.6",
|
||||
"fumadocs-mdx": "11.8.0",
|
||||
"fumadocs-typescript": "^4.0.6",
|
||||
"fumadocs-ui": "15.0.15",
|
||||
"fumadocs-ui": "15.7.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"input-otp": "^1.4.1",
|
||||
"jotai": "^2.12.1",
|
||||
"jotai": "^2.13.1",
|
||||
"js-beautify": "^1.15.4",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"lucide-react": "^0.477.0",
|
||||
"motion": "^12.4.10",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.5.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.14.1",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
defineCollections,
|
||||
} from "fumadocs-mdx/config";
|
||||
import { z } from "zod";
|
||||
import { remarkInstall } from "fumadocs-docgen";
|
||||
import { remarkAutoTypeTable, createGenerator } from "fumadocs-typescript";
|
||||
import { remarkNpm } from "fumadocs-core/mdx-plugins";
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: "./content/docs",
|
||||
@@ -44,7 +44,7 @@ export default defineConfig({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
[
|
||||
remarkInstall,
|
||||
remarkNpm,
|
||||
{
|
||||
persist: {
|
||||
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