feat(docs): add docs and restructure

This commit is contained in:
BennyKok 2023-12-28 00:06:32 +08:00
parent afc67bc0b9
commit 498374195d
56 changed files with 3751 additions and 54 deletions

Binary file not shown.

View File

@ -1,15 +1,17 @@
import type { Config } from 'drizzle-kit'; import { config } from "dotenv";
import { config } from 'dotenv'; import type { Config } from "drizzle-kit";
config({ config({
path: `.env.local`, path: `.env.local`,
}); });
export default { export default {
schema: './src/db/schema.ts', schema: "./src/db/schema.ts",
driver: 'pg', driver: "pg",
out: './drizzle', out: "./drizzle",
dbCredentials: { dbCredentials: {
connectionString: (process.env.POSTGRES_URL as string) + ( process.env.POSTGRES_SSL !== "false" ? '?ssl=true' : ""), connectionString:
(process.env.POSTGRES_URL as string) +
(process.env.POSTGRES_SSL !== "false" ? "?ssl=true" : ""),
}, },
} satisfies Config; } satisfies Config;

9
web/mdx-components.tsx Normal file
View File

@ -0,0 +1,9 @@
import * as mdxComponents from "@/components/docs/mdx";
import { type MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents) {
return {
...components,
...mdxComponents,
};
}

View File

@ -1,8 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
};
module.exports = nextConfig;

23
web/next.config.mjs Normal file
View File

@ -0,0 +1,23 @@
import { recmaPlugins } from "./src/mdx/recma.mjs";
import { rehypePlugins } from "./src/mdx/rehype.mjs";
import { remarkPlugins } from "./src/mdx/remark.mjs";
import withSearch from "./src/mdx/search.mjs";
import nextMDX from "@next/mdx";
const withMDX = nextMDX({
options: {
remarkPlugins,
rehypePlugins,
recmaPlugins,
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"],
eslint: {
ignoreDuringBuilds: true,
},
};
export default withSearch(withMDX(nextConfig));

View File

@ -15,11 +15,17 @@
"db-dev": "bun run db-up && bun run migrate-local" "db-dev": "bun run db-up && bun run migrate-local"
}, },
"dependencies": { "dependencies": {
"@algolia/autocomplete-core": "^1.13.0",
"@aws-sdk/client-s3": "^3.472.0", "@aws-sdk/client-s3": "^3.472.0",
"@aws-sdk/s3-request-presigner": "^3.472.0", "@aws-sdk/s3-request-presigner": "^3.472.0",
"@clerk/nextjs": "^4.27.4", "@clerk/nextjs": "^4.27.4",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"@hookform/resolvers": "^3.3.2", "@hookform/resolvers": "^3.3.2",
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@neondatabase/serverless": "^0.6.0", "@neondatabase/serverless": "^0.6.0",
"@next/mdx": "^14.0.4",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
@ -34,41 +40,54 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-table": "^8.10.7", "@tanstack/react-table": "^8.10.7",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/react-highlight-words": "^0.16.7",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"acorn": "^8.11.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^3.0.5", "date-fns": "^3.0.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.31",
"framer-motion": "^10.16.16",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"next": "14.0.3", "next": "14.0.3",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"next-themes": "^0.2.1",
"next-usequerystate": "^1.13.2", "next-usequerystate": "^1.13.2",
"react": "^18", "react": "^18",
"react-day-picker": "^8.9.1", "react-day-picker": "^8.9.1",
"react-dom": "^18", "react-dom": "^18",
"react-highlight-words": "^0.20.0",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.48.2",
"react-use-websocket": "^4.5.0", "react-use-websocket": "^4.5.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-mdx": "^3.0.0",
"shiki": "^0.14.7",
"shikiji": "^0.9.3", "shikiji": "^0.9.3",
"simple-functional-loader": "^1.2.1",
"sonner": "^1.2.4", "sonner": "^1.2.4",
"swr": "^2.2.4", "swr": "^2.2.4",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"unist-util-filter": "^5.0.1",
"unist-util-visit": "^5.0.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zod": "^3.22.4", "zod": "^3.22.4",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.6.0",
"eslint-config-turbo": "latest",
"eslint-plugin-prettier": "4.2.1",
"prettier-plugin-tailwindcss": "0.2.5",
"@trivago/prettier-plugin-sort-imports": "4.1.1", "@trivago/prettier-plugin-sort-imports": "4.1.1",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
@ -80,10 +99,16 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"drizzle-kit": "^0.20.6", "drizzle-kit": "^0.20.6",
"eslint": "8.34.0", "eslint": "8.34.0",
"eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.6.0",
"eslint-config-turbo": "latest",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"postcss": "^8", "postcss": "^8",
"postgres": "^3.4.3", "postgres": "^3.4.3",
"prettier": "2.8.6", "prettier": "2.8.6",
"prettier-plugin-tailwindcss": "0.2.5",
"sharp": "^0.33.1",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
} }

View File

@ -1,4 +1,4 @@
import { parseDataSafe } from "../../../lib/parseDataSafe"; import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { handleResourceUpload } from "@/server/resource"; import { handleResourceUpload } from "@/server/resource";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { z } from "zod"; import { z } from "zod";

View File

@ -1,5 +1,5 @@
import { parseDataSafe } from "../../../lib/parseDataSafe"; import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { createRun } from "../../../server/createRun"; import { createRun } from "../../../../server/createRun";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { deploymentsTable } from "@/db/schema"; import { deploymentsTable } from "@/db/schema";
import { isKeyRevoked } from "@/server/curdApiKeys"; import { isKeyRevoked } from "@/server/curdApiKeys";

View File

@ -1,4 +1,4 @@
import { parseDataSafe } from "../../../lib/parseDataSafe"; import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { workflowRunOutputs, workflowRunsTable } from "@/db/schema"; import { workflowRunOutputs, workflowRunsTable } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@ -1,4 +1,4 @@
import { parseJWT } from "../../../server/parseJWT"; import { parseJWT } from "../../../../server/parseJWT";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { import {
workflowAPIType, workflowAPIType,

View File

@ -1,4 +1,4 @@
import { getFileDownloadUrl } from "../../../server/getFileDownloadUrl"; import { getFileDownloadUrl } from "../../../../server/getFileDownloadUrl";
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,3 +1,21 @@
@layer base {
:root {
--shiki-color-text: theme('colors.white');
--shiki-token-constant: theme('colors.emerald.300');
--shiki-token-string: theme('colors.emerald.300');
--shiki-token-comment: theme('colors.zinc.500');
--shiki-token-keyword: theme('colors.sky.300');
--shiki-token-parameter: theme('colors.pink.300');
--shiki-token-function: theme('colors.violet.300');
--shiki-token-string-expression: theme('colors.emerald.300');
--shiki-token-punctuation: theme('colors.zinc.200');
}
[inert] ::-webkit-scrollbar {
display: none;
}
}
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -57,6 +57,13 @@ export default function RootLayout({
<NavbarRight /> <NavbarRight />
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<Button
asChild
variant="link"
className="rounded-full aspect-square p-2 mr-4"
>
<a href="/docs">Docs</a>
</Button>
<UserButton /> <UserButton />
<Button <Button
asChild asChild

View File

@ -1,5 +1,5 @@
import { DeploymentsTable, RunsTable } from "../../../components/RunsTable"; import { DeploymentsTable, RunsTable } from "../../../../components/RunsTable";
import { findFirstTableWithVersion } from "../../../server/findFirstTableWithVersion"; import { findFirstTableWithVersion } from "../../../../server/findFirstTableWithVersion";
import { MachinesWSMain } from "@/components/MachinesWS"; import { MachinesWSMain } from "@/components/MachinesWS";
import { VersionDetails } from "@/components/VersionDetails"; import { VersionDetails } from "@/components/VersionDetails";
import { import {

View File

@ -0,0 +1,49 @@
export const metadata = {
title: 'Quickstart',
description:
'This guide will get you all set up and ready to use the Protocol API. Well cover how to get started an API client and how to make your first API request.',
}
# Getting stated
Install Comfy Deploy's plugin on your local machine to get started with deploying workflow.
<CodeGroup>
```bash {{ title: 'git' }}
cd custom_nodes
git clone https://github.com/BennyKok/comfyui-deploy.git
```
```js {{ language: 'js', title: 'ComfyUI Manager' }}
// Install with ComfyUI Manager
Search `ComfyUI Deploy`
```
</CodeGroup>
## ComfyUI Manager
Install from ComfyUI Manager
<Image src="https://media.discordapp.net/attachments/1187301526646030386/1189400730101104660/CleanShot_2023-12-27_at_10.53.272x.png"/>
## Setup your workflow
Add ComfyDeploy node
<Image className="max-w-xl" src="https://cdn.discordapp.com/attachments/1187301526646030386/1189400819741761546/CleanShot_2023-12-27_at_10.54.052x.png"/>
Set a name for your workflow
<Image className="max-w-xl" src="https://cdn.discordapp.com/attachments/1187301526646030386/1189400882773766184/CleanShot_2023-12-27_at_10.54.212x.png"/>
Set up your API key
<Image className="w-fit max-h-48" src="https://cdn.discordapp.com/attachments/1189594955279245403/1189594974149431326/CleanShot_2023-12-27_at_23.45.13.png"/>
Open the settings panel and then set your API key here, if you dont have one, create here <a href="/api-keys">Create API Key</a>
<Image className="max-w-lg" src="https://cdn.discordapp.com/attachments/1187301526646030386/1189401104472080514/CleanShot_2023-12-27_at_10.54.372x.png"/>
## What's next?
Now, hit the Deploy button and your workflow will be deployed to ComfyUI's server. Check out <a href="/workflows">your workflows here</a>.

View File

@ -0,0 +1,40 @@
import { Providers } from "./providers";
import "@/app/(app)/globals.css";
import { Layout } from "@/components/docs/Layout";
import { type Section } from "@/components/docs/SectionProvider";
import glob from "fast-glob";
import { type Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s - Protocol API Reference",
default: "Protocol API Reference",
},
};
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const pages = await glob("**/*.mdx", { cwd: "src/app/(docs)/docs" });
const allSectionsEntries = (await Promise.all(
pages.map(async (filename) => [
`/${filename.replace(/(^|\/)page\.mdx$/, "")}`,
(await import(`./${filename}`)).sections,
])
)) as Array<[string, Array<Section>]>;
const allSections = Object.fromEntries(allSectionsEntries);
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<body className="flex min-h-full bg-white antialiased dark:bg-zinc-900">
<Providers>
<div className="w-full">
<Layout allSections={allSections}>{children}</Layout>
</div>
</Providers>
</body>
</html>
);
}

View File

@ -0,0 +1,24 @@
import { Button } from "@/components/docs/Button";
import { HeroPattern } from "@/components/docs/HeroPattern";
export default function NotFound() {
return (
<>
<HeroPattern />
<div className="mx-auto flex h-full max-w-xl flex-col items-center justify-center py-16 text-center">
<p className="text-sm font-semibold text-zinc-900 dark:text-white">
404
</p>
<h1 className="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">
Page not found
</h1>
<p className="mt-2 text-base text-zinc-600 dark:text-zinc-400">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" arrow="right" className="mt-8">
Back to docs
</Button>
</div>
</>
);
}

View File

@ -0,0 +1,34 @@
import { Guides } from '@/components/docs/Guides'
import { Resources } from '@/components/docs/Resources'
import { HeroPattern } from '@/components/docs/HeroPattern'
export const metadata = {
title: 'Comfy Deploy Documentation',
description:
'Get your comfy deploy setup running in minutes. Learn how to deploy your generative workflow to comfy deploy.',
}
<HeroPattern />
# Introduction
Open source comfyui deployment platform, a vercel for generative workflow infra.
<div className="not-prose mb-16 mt-6 flex gap-3">
<Button href="/docs/install" arrow="right">
<>Install</>
</Button>
<Button href="/api-keys" variant="outline">
<>Create API Key</>
</Button>
</div>
{/* ## Getting started {{ anchor: false }}
Get started by first installing Comfy Deploy plugin
<div className="not-prose">
<Button href="/api-keys" variant="text" arrow="right">
<>Get your API key</>
</Button>
</div> */}

View File

@ -0,0 +1,37 @@
"use client";
import { ThemeProvider, useTheme } from "next-themes";
import { useEffect } from "react";
function ThemeWatcher() {
const { resolvedTheme, setTheme } = useTheme();
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: dark)");
function onMediaChange() {
const systemTheme = media.matches ? "dark" : "light";
if (resolvedTheme === systemTheme) {
setTheme("system");
}
}
onMediaChange();
media.addEventListener("change", onMediaChange);
return () => {
media.removeEventListener("change", onMediaChange);
};
}, [resolvedTheme, setTheme]);
return null;
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" disableTransitionOnChange>
<ThemeWatcher />
{children}
</ThemeProvider>
);
}

View File

@ -96,7 +96,7 @@ export function MachineSelect({
setMachine(v); setMachine(v);
}} }}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px] text-start">
<SelectValue placeholder="Select a machine" /> <SelectValue placeholder="Select a machine" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@ -0,0 +1,82 @@
import clsx from "clsx";
import Link from "next/link";
function ArrowIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9"
/>
</svg>
);
}
const variantStyles = {
primary:
"rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300",
secondary:
"rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300",
filled:
"rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-500 dark:text-white dark:hover:bg-emerald-400",
outline:
"rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white",
text: "text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-500",
};
type ButtonProps = {
variant?: keyof typeof variantStyles;
arrow?: "left" | "right";
} & (
| React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
);
export function Button({
variant = "primary",
className,
children,
arrow,
...props
}: ButtonProps) {
className = clsx(
"inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition items-center ",
variantStyles[variant],
className
);
const arrowIcon = (
<ArrowIcon
className={clsx(
"h-5 w-5",
variant === "text" && "relative top-px",
arrow === "left" && "-ml-1 rotate-180",
arrow === "right" && "-mr-1"
)}
/>
);
const inner = (
<>
{arrow === "left" && arrowIcon}
{children}
{arrow === "right" && arrowIcon}
</>
);
if (typeof props.href === "undefined") {
return (
<button className={className} {...props}>
{inner}
</button>
);
}
return (
<Link className={className} {...props}>
{inner}
</Link>
);
}

View File

@ -0,0 +1,380 @@
"use client";
import { Tag } from "@/components/docs/Tag";
import { Tab } from "@headlessui/react";
import clsx from "clsx";
import {
Children,
createContext,
isValidElement,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { create } from "zustand";
const languageNames: Record<string, string> = {
js: "JavaScript",
ts: "TypeScript",
javascript: "JavaScript",
typescript: "TypeScript",
php: "PHP",
python: "Python",
ruby: "Ruby",
go: "Go",
};
function getPanelTitle({
title,
language,
}: {
title?: string;
language?: string;
}) {
if (title) {
return title;
}
if (language && language in languageNames) {
return languageNames[language];
}
return "Code";
}
function ClipboardIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
/>
<path
fill="none"
strokeLinejoin="round"
d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
/>
</svg>
);
}
function CopyButton({ code }: { code: string }) {
const [copyCount, setCopyCount] = useState(0);
const copied = copyCount > 0;
useEffect(() => {
if (copyCount > 0) {
const timeout = setTimeout(() => setCopyCount(0), 1000);
return () => {
clearTimeout(timeout);
};
}
}, [copyCount]);
return (
<button
type="button"
className={clsx(
"group/button absolute right-4 top-3.5 overflow-hidden rounded-full py-1 pl-2 pr-3 text-2xs font-medium opacity-0 backdrop-blur transition focus:opacity-100 group-hover:opacity-100",
copied
? "bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20"
: "bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5"
)}
onClick={() => {
window.navigator.clipboard.writeText(code).then(() => {
setCopyCount((count) => count + 1);
});
}}
>
<span
aria-hidden={copied}
className={clsx(
"pointer-events-none flex items-center gap-0.5 text-zinc-400 transition duration-300",
copied && "-translate-y-1.5 opacity-0"
)}
>
<ClipboardIcon className="h-5 w-5 fill-zinc-500/20 stroke-zinc-500 transition-colors group-hover/button:stroke-zinc-400" />
Copy
</span>
<span
aria-hidden={!copied}
className={clsx(
"pointer-events-none absolute inset-0 flex items-center justify-center text-emerald-400 transition duration-300",
!copied && "translate-y-1.5 opacity-0"
)}
>
Copied!
</span>
</button>
);
}
function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) {
if (!tag && !label) {
return null;
}
return (
<div className="flex h-9 items-center gap-2 border-y border-b-white/7.5 border-t-transparent bg-white/2.5 bg-zinc-900 px-4 dark:border-b-white/5 dark:bg-white/1">
{tag && (
<div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div>
)}
{tag && label && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
)}
{label && (
<span className="font-mono text-xs text-zinc-400">{label}</span>
)}
</div>
);
}
function CodePanel({
children,
tag,
label,
code,
}: {
children: React.ReactNode;
tag?: string;
label?: string;
code?: string;
}) {
const child = Children.only(children);
if (isValidElement(child)) {
tag = child.props.tag ?? tag;
label = child.props.label ?? label;
code = child.props.code ?? code;
}
if (!code) {
throw new Error(
"`CodePanel` requires a `code` prop, or a child with a `code` prop."
);
}
return (
<div className="group dark:bg-white/2.5">
<CodePanelHeader tag={tag} label={label} />
<div className="relative">
<pre className="overflow-x-auto p-4 text-xs text-white">{children}</pre>
<CopyButton code={code} />
</div>
</div>
);
}
function CodeGroupHeader({
title,
children,
selectedIndex,
}: {
title: string;
children: React.ReactNode;
selectedIndex: number;
}) {
const hasTabs = Children.count(children) > 1;
if (!title && !hasTabs) {
return null;
}
return (
<div className="flex min-h-[calc(theme(spacing.12)+1px)] flex-wrap items-start gap-x-4 border-b border-zinc-700 bg-zinc-800 px-4 dark:border-zinc-800 dark:bg-transparent">
{title && (
<h3 className="mr-auto pt-3 text-xs font-semibold text-white">
{title}
</h3>
)}
{hasTabs && (
<Tab.List className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => (
<Tab
className={clsx(
"border-b py-3 transition ui-not-focus-visible:outline-none",
childIndex === selectedIndex
? "border-emerald-500 text-emerald-400"
: "border-transparent text-zinc-400 hover:text-zinc-300"
)}
>
{getPanelTitle(isValidElement(child) ? child.props : {})}
</Tab>
))}
</Tab.List>
)}
</div>
);
}
function CodeGroupPanels({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodePanel>) {
const hasTabs = Children.count(children) > 1;
if (hasTabs) {
return (
<Tab.Panels>
{Children.map(children, (child) => (
<Tab.Panel>
<CodePanel {...props}>{child}</CodePanel>
</Tab.Panel>
))}
</Tab.Panels>
);
}
return <CodePanel {...props}>{children}</CodePanel>;
}
function usePreventLayoutShift() {
const positionRef = useRef<HTMLElement>(null);
const rafRef = useRef<number>();
useEffect(() => {
return () => {
if (typeof rafRef.current !== "undefined") {
window.cancelAnimationFrame(rafRef.current);
}
};
}, []);
return {
positionRef,
preventLayoutShift(callback: () => void) {
if (!positionRef.current) {
return;
}
const initialTop = positionRef.current.getBoundingClientRect().top;
callback();
rafRef.current = window.requestAnimationFrame(() => {
const newTop =
positionRef.current?.getBoundingClientRect().top ?? initialTop;
window.scrollBy(0, newTop - initialTop);
});
},
};
}
const usePreferredLanguageStore = create<{
preferredLanguages: Array<string>;
addPreferredLanguage: (language: string) => void;
}>()((set) => ({
preferredLanguages: [],
addPreferredLanguage: (language) =>
set((state) => ({
preferredLanguages: [
...state.preferredLanguages.filter(
(preferredLanguage) => preferredLanguage !== language
),
language,
],
})),
}));
function useTabGroupProps(availableLanguages: Array<string>) {
const { preferredLanguages, addPreferredLanguage } =
usePreferredLanguageStore();
const [selectedIndex, setSelectedIndex] = useState(0);
const activeLanguage = [...availableLanguages].sort(
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a)
)[0];
const languageIndex = availableLanguages.indexOf(activeLanguage);
const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex;
if (newSelectedIndex !== selectedIndex) {
setSelectedIndex(newSelectedIndex);
}
const { positionRef, preventLayoutShift } = usePreventLayoutShift();
return {
as: "div" as const,
ref: positionRef,
selectedIndex,
onChange: (newSelectedIndex: number) => {
preventLayoutShift(() =>
addPreferredLanguage(availableLanguages[newSelectedIndex])
);
},
};
}
const CodeGroupContext = createContext(false);
export function CodeGroup({
children,
title,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroupPanels> & { title: string }) {
const languages =
Children.map(children, (child) =>
getPanelTitle(isValidElement(child) ? child.props : {})
) ?? [];
const tabGroupProps = useTabGroupProps(languages);
const hasTabs = Children.count(children) > 1;
const containerClassName =
"my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10";
const header = (
<CodeGroupHeader title={title} selectedIndex={tabGroupProps.selectedIndex}>
{children}
</CodeGroupHeader>
);
const panels = <CodeGroupPanels {...props}>{children}</CodeGroupPanels>;
return (
<CodeGroupContext.Provider value={true}>
{hasTabs ? (
<Tab.Group {...tabGroupProps} className={containerClassName}>
<div className="not-prose">
{header}
{panels}
</div>
</Tab.Group>
) : (
<div className={containerClassName}>
<div className="not-prose">
{header}
{panels}
</div>
</div>
)}
</CodeGroupContext.Provider>
);
}
export function Code({
children,
...props
}: React.ComponentPropsWithoutRef<"code">) {
const isGrouped = useContext(CodeGroupContext);
if (isGrouped) {
if (typeof children !== "string") {
throw new Error(
"`Code` children must be a string when nested inside a `CodeGroup`."
);
}
return <code {...props} dangerouslySetInnerHTML={{ __html: children }} />;
}
return <code {...props}>{children}</code>;
}
export function Pre({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroup>) {
const isGrouped = useContext(CodeGroupContext);
if (isGrouped) {
return children;
}
return <CodeGroup {...props}>{children}</CodeGroup>;
}

View File

@ -0,0 +1,105 @@
'use client'
import { forwardRef, Fragment, useState } from 'react'
import { Transition } from '@headlessui/react'
function CheckIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="10" strokeWidth="0" />
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="m6.75 10.813 2.438 2.437c1.218-4.469 4.062-6.5 4.062-6.5"
/>
</svg>
)
}
function FeedbackButton(
props: Omit<React.ComponentPropsWithoutRef<'button'>, 'type' | 'className'>,
) {
return (
<button
type="submit"
className="px-3 text-sm font-medium text-zinc-600 transition hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white"
{...props}
/>
)
}
const FeedbackForm = forwardRef<
React.ElementRef<'form'>,
Pick<React.ComponentPropsWithoutRef<'form'>, 'onSubmit'>
>(function FeedbackForm({ onSubmit }, ref) {
return (
<form
ref={ref}
onSubmit={onSubmit}
className="absolute inset-0 flex items-center justify-center gap-6 md:justify-start"
>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
Was this page helpful?
</p>
<div className="group grid h-8 grid-cols-[1fr,1px,1fr] overflow-hidden rounded-full border border-zinc-900/10 dark:border-white/10">
<FeedbackButton data-response="yes">Yes</FeedbackButton>
<div className="bg-zinc-900/10 dark:bg-white/10" />
<FeedbackButton data-response="no">No</FeedbackButton>
</div>
</form>
)
})
const FeedbackThanks = forwardRef<React.ElementRef<'div'>>(
function FeedbackThanks(_props, ref) {
return (
<div
ref={ref}
className="absolute inset-0 flex justify-center md:justify-start"
>
<div className="flex items-center gap-3 rounded-full bg-emerald-50/50 py-1 pl-1.5 pr-3 text-sm text-emerald-900 ring-1 ring-inset ring-emerald-500/20 dark:bg-emerald-500/5 dark:text-emerald-200 dark:ring-emerald-500/30">
<CheckIcon className="h-5 w-5 flex-none fill-emerald-500 stroke-white dark:fill-emerald-200/20 dark:stroke-emerald-200" />
Thanks for your feedback!
</div>
</div>
)
},
)
export function Feedback() {
let [submitted, setSubmitted] = useState(false)
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
// event.nativeEvent.submitter.dataset.response
// => "yes" or "no"
setSubmitted(true)
}
return (
<div className="relative h-8">
<Transition
show={!submitted}
as={Fragment}
leaveFrom="opacity-100"
leaveTo="opacity-0"
leave="pointer-events-none duration-300"
>
<FeedbackForm onSubmit={onSubmit} />
</Transition>
<Transition
show={submitted}
as={Fragment}
enterFrom="opacity-0"
enterTo="opacity-100"
enter="delay-150 duration-300"
>
<FeedbackThanks />
</Transition>
</div>
)
}

View File

@ -0,0 +1,144 @@
"use client";
import { Button } from "@/components/docs/Button";
import { navigation } from "@/components/docs/Navigation";
import Link from "next/link";
import { usePathname } from "next/navigation";
function PageLink({
label,
page,
previous = false,
}: {
label: string;
page: { href: string; title: string };
previous?: boolean;
}) {
return (
<>
<Button
href={page.href}
aria-label={`${label}: ${page.title}`}
variant="secondary"
arrow={previous ? "left" : "right"}
>
{label}
</Button>
<Link
href={page.href}
tabIndex={-1}
aria-hidden="true"
className="text-base font-semibold text-zinc-900 transition hover:text-zinc-600 dark:text-white dark:hover:text-zinc-300"
>
{page.title}
</Link>
</>
);
}
function PageNavigation() {
const pathname = usePathname();
const allPages = navigation.flatMap((group) => group.links);
const currentPageIndex = allPages.findIndex((page) => page.href === pathname);
if (currentPageIndex === -1) {
return null;
}
const previousPage = allPages[currentPageIndex - 1];
const nextPage = allPages[currentPageIndex + 1];
if (!previousPage && !nextPage) {
return null;
}
return (
<div className="flex">
{previousPage && (
<div className="flex flex-col items-start gap-3">
<PageLink label="Previous" page={previousPage} previous />
</div>
)}
{nextPage && (
<div className="ml-auto flex flex-col items-end gap-3">
<PageLink label="Next" page={nextPage} />
</div>
)}
</div>
);
}
function TwitterIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M16.712 6.652c.01.146.01.29.01.436 0 4.449-3.267 9.579-9.242 9.579v-.003a8.963 8.963 0 0 1-4.98-1.509 6.379 6.379 0 0 0 4.807-1.396c-1.39-.027-2.608-.966-3.035-2.337.487.097.99.077 1.467-.059-1.514-.316-2.606-1.696-2.606-3.3v-.041c.45.26.956.404 1.475.42C3.18 7.454 2.74 5.486 3.602 3.947c1.65 2.104 4.083 3.382 6.695 3.517a3.446 3.446 0 0 1 .94-3.217 3.172 3.172 0 0 1 4.596.148 6.38 6.38 0 0 0 2.063-.817 3.357 3.357 0 0 1-1.428 1.861 6.283 6.283 0 0 0 1.865-.53 6.735 6.735 0 0 1-1.62 1.744Z" />
</svg>
);
}
function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 1.667c-4.605 0-8.334 3.823-8.334 8.544 0 3.78 2.385 6.974 5.698 8.106.417.075.573-.182.573-.406 0-.203-.011-.875-.011-1.592-2.093.397-2.635-.522-2.802-1.002-.094-.246-.5-1.005-.854-1.207-.291-.16-.708-.556-.01-.567.656-.01 1.124.62 1.281.876.75 1.292 1.948.93 2.427.705.073-.555.291-.93.531-1.143-1.854-.213-3.791-.95-3.791-4.218 0-.929.322-1.698.854-2.296-.083-.214-.375-1.09.083-2.265 0 0 .698-.224 2.292.876a7.576 7.576 0 0 1 2.083-.288c.709 0 1.417.096 2.084.288 1.593-1.11 2.291-.875 2.291-.875.459 1.174.167 2.05.084 2.263.53.599.854 1.357.854 2.297 0 3.278-1.948 4.005-3.802 4.219.302.266.563.78.563 1.58 0 1.143-.011 2.061-.011 2.35 0 .224.156.491.573.405a8.365 8.365 0 0 0 4.11-3.116 8.707 8.707 0 0 0 1.567-4.99c0-4.721-3.73-8.545-8.334-8.545Z"
/>
</svg>
);
}
function DiscordIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M16.238 4.515a14.842 14.842 0 0 0-3.664-1.136.055.055 0 0 0-.059.027 10.35 10.35 0 0 0-.456.938 13.702 13.702 0 0 0-4.115 0 9.479 9.479 0 0 0-.464-.938.058.058 0 0 0-.058-.027c-1.266.218-2.497.6-3.664 1.136a.052.052 0 0 0-.024.02C1.4 8.023.76 11.424 1.074 14.782a.062.062 0 0 0 .024.042 14.923 14.923 0 0 0 4.494 2.272.058.058 0 0 0 .064-.02c.346-.473.654-.972.92-1.496a.057.057 0 0 0-.032-.08 9.83 9.83 0 0 1-1.404-.669.058.058 0 0 1-.029-.046.058.058 0 0 1 .023-.05c.094-.07.189-.144.279-.218a.056.056 0 0 1 .058-.008c2.946 1.345 6.135 1.345 9.046 0a.056.056 0 0 1 .059.007c.09.074.184.149.28.22a.058.058 0 0 1 .023.049.059.059 0 0 1-.028.046 9.224 9.224 0 0 1-1.405.669.058.058 0 0 0-.033.033.056.056 0 0 0 .002.047c.27.523.58 1.022.92 1.495a.056.056 0 0 0 .062.021 14.878 14.878 0 0 0 4.502-2.272.055.055 0 0 0 .016-.018.056.056 0 0 0 .008-.023c.375-3.883-.63-7.256-2.662-10.246a.046.046 0 0 0-.023-.021Zm-9.223 8.221c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.717 1.814-1.618 1.814Zm5.981 0c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.71 1.814-1.618 1.814Z" />
</svg>
);
}
function SocialLink({
href,
icon: Icon,
children,
}: {
href: string;
icon: React.ComponentType<{ className?: string }>;
children: React.ReactNode;
}) {
return (
<Link href={href} className="group">
<span className="sr-only">{children}</span>
<Icon className="h-5 w-5 fill-zinc-700 transition group-hover:fill-zinc-900 dark:group-hover:fill-zinc-500" />
</Link>
);
}
function SmallPrint() {
return (
<div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row">
<p className="text-xs text-zinc-600 dark:text-zinc-400">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
</p>
<div className="flex gap-4">
<SocialLink href="#" icon={TwitterIcon}>
Follow us on Twitter
</SocialLink>
<SocialLink href="#" icon={GitHubIcon}>
Follow us on GitHub
</SocialLink>
<SocialLink href="#" icon={DiscordIcon}>
Join our Discord server
</SocialLink>
</div>
</div>
);
}
export function Footer() {
return (
<footer className="mx-auto w-full max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
<PageNavigation />
<SmallPrint />
</footer>
);
}

View File

@ -0,0 +1,55 @@
import { useId } from "react";
export function GridPattern({
width,
height,
x,
y,
squares,
...props
}: React.ComponentPropsWithoutRef<"svg"> & {
width: number;
height: number;
x: string | number;
y: string | number;
squares: Array<[x: number, y: number]>;
}) {
const patternId = useId();
return (
<svg aria-hidden="true" {...props}>
<defs>
<pattern
id={patternId}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path d={`M.5 ${height}V.5H${width}`} fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill={`url(#${patternId})`}
/>
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width + 1}
height={height + 1}
x={x * width}
y={y * height}
/>
))}
</svg>
)}
</svg>
);
}

View File

@ -0,0 +1,54 @@
import { Button } from "@/components/docs/Button";
import { Heading } from "@/components/docs/Heading";
const guides = [
{
href: "/authentication",
name: "Authentication",
description: "Learn how to authenticate your API requests.",
},
{
href: "/pagination",
name: "Pagination",
description: "Understand how to work with paginated responses.",
},
{
href: "/errors",
name: "Errors",
description:
"Read about the different types of errors returned by the API.",
},
{
href: "/webhooks",
name: "Webhooks",
description:
"Learn how to programmatically configure webhooks for your app.",
},
];
export function Guides() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="guides">
Guides
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-8 border-t border-zinc-900/5 pt-10 dark:border-white/5 sm:grid-cols-2 xl:grid-cols-4">
{guides.map((guide) => (
<div key={guide.href}>
<h3 className="text-sm font-semibold text-zinc-900 dark:text-white">
{guide.name}
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{guide.description}
</p>
<p className="mt-4">
<Button href={guide.href} variant="text" arrow="right">
Read more
</Button>
</p>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,99 @@
import {
MobileNavigation,
useIsInsideMobileNavigation,
} from "@/components/docs/MobileNavigation";
import { useMobileNavigationStore } from "@/components/docs/MobileNavigation";
import { MobileSearch, Search } from "@/components/docs/Search";
import { ThemeToggle } from "@/components/docs/ThemeToggle";
import clsx from "clsx";
import { motion, useScroll, useTransform } from "framer-motion";
import meta from "next-gen/config";
import Link from "next/link";
import { forwardRef } from "react";
function TopLevelNavItem({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<li>
<Link
href={href}
className="text-sm leading-5 text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
>
{children}
</Link>
</li>
);
}
export const Header = forwardRef<
React.ElementRef<"div">,
{ className?: string }
>(function Header({ className }, ref) {
const { isOpen: mobileNavIsOpen } = useMobileNavigationStore();
const isInsideMobileNavigation = useIsInsideMobileNavigation();
const { scrollY } = useScroll();
const bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9]);
const bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8]);
return (
<motion.div
ref={ref}
className={clsx(
className,
"fixed inset-x-0 top-0 z-50 flex h-14 items-center justify-between gap-12 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80",
!isInsideMobileNavigation &&
"backdrop-blur-sm dark:backdrop-blur lg:left-72 xl:left-80",
isInsideMobileNavigation
? "bg-white dark:bg-zinc-900"
: "bg-white/[var(--bg-opacity-light)] dark:bg-zinc-900/[var(--bg-opacity-dark)]"
)}
style={
{
"--bg-opacity-light": bgOpacityLight,
"--bg-opacity-dark": bgOpacityDark,
} as React.CSSProperties
}
>
<div
className={clsx(
"absolute inset-x-0 top-full h-px transition",
(isInsideMobileNavigation || !mobileNavIsOpen) &&
"bg-zinc-900/7.5 dark:bg-white/7.5"
)}
/>
<Search />
<div className="flex items-center gap-5 lg:hidden">
<MobileNavigation />
{/* <Link href="/" aria-label="Home">
<Logo className="h-6" />
</Link> */}
<a className="font-bold text-md md:text-lg hover:underline" href="/">
{meta.name}
</a>
</div>
<div className="flex items-center gap-5">
{/* <nav className="hidden md:block">
<ul role="list" className="flex items-center gap-8">
<TopLevelNavItem href="/">API</TopLevelNavItem>
<TopLevelNavItem href="#">Documentation</TopLevelNavItem>
<TopLevelNavItem href="#">Support</TopLevelNavItem>
</ul>
</nav> */}
<div className="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
<div className="flex gap-4">
<MobileSearch />
<ThemeToggle />
</div>
{/* <div className="hidden min-[416px]:contents">
<Button href="#">Sign in</Button>
</div> */}
</div>
</motion.div>
);
});

View File

@ -0,0 +1,116 @@
"use client";
import { useSectionStore } from "@/components/docs/SectionProvider";
import { Tag } from "@/components/docs/Tag";
import { remToPx } from "@/lib/remToPx";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
function AnchorIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="m6.5 11.5-.964-.964a3.535 3.535 0 1 1 5-5l.964.964m2 2 .964.964a3.536 3.536 0 0 1-5 5L8.5 13.5m0-5 3 3" />
</svg>
);
}
function Eyebrow({ tag, label }: { tag?: string; label?: string }) {
if (!tag && !label) {
return null;
}
return (
<div className="flex items-center gap-x-3">
{tag && <Tag>{tag}</Tag>}
{tag && label && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
)}
{label && (
<span className="font-mono text-xs text-zinc-400">{label}</span>
)}
</div>
);
}
function Anchor({
id,
inView,
children,
}: {
id: string;
inView: boolean;
children: React.ReactNode;
}) {
return (
<Link
href={`#${id}`}
className="group text-inherit no-underline hover:text-inherit"
>
{inView && (
<div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(2.625rem+0.5px+50%-min(50%,calc(theme(maxWidth.lg)+theme(spacing.8))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
</div>
</div>
)}
{children}
</Link>
);
}
export function Heading<Level extends 2 | 3>({
children,
tag,
label,
level,
anchor = true,
...props
}: React.ComponentPropsWithoutRef<`h${Level}`> & {
id: string;
tag?: string;
label?: string;
level?: Level;
anchor?: boolean;
}) {
level = level ?? (2 as Level);
const Component = `h${level}` as "h2" | "h3";
const ref = useRef<HTMLHeadingElement>(null);
const registerHeading = useSectionStore((s) => s.registerHeading);
const inView = useInView(ref, {
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
amount: "all",
});
useEffect(() => {
if (level === 2) {
registerHeading({ id: props.id, ref, offsetRem: tag || label ? 8 : 6 });
}
});
return (
<>
<Eyebrow tag={tag} label={label} />
<Component
ref={ref}
className={tag || label ? "mt-2 scroll-mt-32" : "scroll-mt-24"}
{...props}
>
{anchor ? (
<Anchor id={props.id} inView={inView}>
{children}
</Anchor>
) : (
children
)}
</Component>
</>
);
}

View File

@ -0,0 +1,32 @@
import { GridPattern } from "@/components/docs/GridPattern";
export function HeroPattern() {
return (
<div className="absolute inset-0 -z-10 mx-0 max-w-none overflow-hidden">
<div className="absolute left-1/2 top-0 ml-[-38rem] h-[25rem] w-[81.25rem] dark:[mask-image:linear-gradient(white,transparent)]">
<div className="absolute inset-0 bg-gradient-to-r from-[#36b49f] to-[#DBFF75] opacity-40 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-[#36b49f]/30 dark:to-[#DBFF75]/30 dark:opacity-100">
<GridPattern
width={72}
height={56}
x={-12}
y={4}
squares={[
[4, 3],
[2, 1],
[7, 3],
[10, 6],
]}
className="absolute inset-x-0 inset-y-[-50%] h-[200%] w-full skew-y-[-18deg] fill-black/40 stroke-black/50 mix-blend-overlay dark:fill-white/2.5 dark:stroke-white/5"
/>
</div>
<svg
viewBox="0 0 1113 440"
aria-hidden="true"
className="absolute left-1/2 top-0 ml-[-19rem] w-[69.5625rem] fill-white blur-[26px] dark:hidden"
>
<path d="M.016 439.5s-9.5-300 434-300S882.516 20 882.516 20V0h230.004v439.5H.016Z" />
</svg>
</div>
</div>
);
}

View File

@ -0,0 +1,52 @@
"use client";
import { Footer } from "@/components/docs/Footer";
import { Header } from "@/components/docs/Header";
import { Navigation } from "@/components/docs/Navigation";
import {
type Section,
SectionProvider,
} from "@/components/docs/SectionProvider";
import { motion } from "framer-motion";
import meta from "next-gen/config";
import Link from "next/link";
import { usePathname } from "next/navigation";
export function Layout({
children,
allSections,
}: {
children: React.ReactNode;
allSections: Record<string, Array<Section>>;
}) {
const pathname = usePathname();
return (
<SectionProvider sections={allSections[pathname] ?? []}>
<div className="h-full lg:ml-72 xl:ml-80">
<motion.header
layoutScroll
className="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex"
>
<div className="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-900/10 lg:px-6 lg:pb-8 lg:pt-4 lg:dark:border-white/10 xl:w-80">
<div className="hidden lg:flex">
<Link
href="/"
className="font-bold text-md md:text-lg hover:underline"
aria-label="Home"
>
{meta.name}
</Link>
</div>
<Header />
<Navigation className="hidden lg:mt-10 lg:block" />
</div>
</motion.header>
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
<main className="flex-auto">{children}</main>
<Footer />
</div>
</div>
</SectionProvider>
);
}

View File

@ -0,0 +1,81 @@
import { Button } from "@/components/docs/Button";
import { Heading } from "@/components/docs/Heading";
import logoGo from "@/images/logos/go.svg";
import logoNode from "@/images/logos/node.svg";
import logoPhp from "@/images/logos/php.svg";
import logoPython from "@/images/logos/python.svg";
import logoRuby from "@/images/logos/ruby.svg";
import Image from "next/image";
const libraries = [
{
href: "#",
name: "PHP",
description:
"A popular general-purpose scripting language that is especially suited to web development.",
logo: logoPhp,
},
{
href: "#",
name: "Ruby",
description:
"A dynamic, open source programming language with a focus on simplicity and productivity.",
logo: logoRuby,
},
{
href: "#",
name: "Node.js",
description:
"Node.js® is an open-source, cross-platform JavaScript runtime environment.",
logo: logoNode,
},
{
href: "#",
name: "Python",
description:
"Python is a programming language that lets you work quickly and integrate systems more effectively.",
logo: logoPython,
},
{
href: "#",
name: "Go",
description:
"An open-source programming language supported by Google with built-in concurrency.",
logo: logoGo,
},
];
export function Libraries() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="official-libraries">
Official libraries
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-t border-zinc-900/5 pt-10 dark:border-white/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3">
{libraries.map((library) => (
<div key={library.name} className="flex flex-row-reverse gap-6">
<div className="flex-auto">
<h3 className="text-sm font-semibold text-zinc-900 dark:text-white">
{library.name}
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{library.description}
</p>
<p className="mt-4">
<Button href={library.href} variant="text" arrow="right">
Read more
</Button>
</p>
</div>
<Image
src={library.logo}
alt=""
className="h-12 w-12"
unoptimized
/>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 99 24" aria-hidden="true" {...props}>
<path
className="fill-emerald-400"
d="M16 8a5 5 0 0 0-5-5H5a5 5 0 0 0-5 5v13.927a1 1 0 0 0 1.623.782l3.684-2.93a4 4 0 0 1 2.49-.87H11a5 5 0 0 0 5-5V8Z"
/>
<path
className="fill-zinc-900 dark:fill-white"
d="M26.538 18h2.654v-3.999h2.576c2.672 0 4.456-1.723 4.456-4.333V9.65c0-2.61-1.784-4.333-4.456-4.333h-5.23V18Zm4.58-10.582c1.52 0 2.416.8 2.416 2.241v.018c0 1.441-.896 2.25-2.417 2.25h-1.925V7.418h1.925ZM38.051 18h2.566v-5.414c0-1.371.923-2.206 2.382-2.206.396 0 .791.061 1.178.15V8.287a3.843 3.843 0 0 0-.958-.123c-1.257 0-2.136.615-2.443 1.661h-.159V8.323h-2.566V18Zm11.55.202c2.979 0 4.772-1.88 4.772-5.036v-.018c0-3.128-1.82-5.036-4.773-5.036-2.953 0-4.772 1.916-4.772 5.036v.018c0 3.146 1.793 5.036 4.772 5.036Zm0-2.013c-1.372 0-2.145-1.116-2.145-3.023v-.018c0-1.89.782-3.023 2.144-3.023 1.354 0 2.145 1.134 2.145 3.023v.018c0 1.907-.782 3.023-2.145 3.023Zm10.52 1.846c.492 0 .967-.053 1.283-.114v-1.907a6.057 6.057 0 0 1-.755.044c-.87 0-1.24-.387-1.24-1.257v-4.544h1.995V8.323H59.41V6.012h-2.592v2.311h-1.495v1.934h1.495v5.133c0 1.88.949 2.645 3.304 2.645Zm7.287.167c2.98 0 4.772-1.88 4.772-5.036v-.018c0-3.128-1.82-5.036-4.772-5.036-2.954 0-4.773 1.916-4.773 5.036v.018c0 3.146 1.793 5.036 4.773 5.036Zm0-2.013c-1.372 0-2.145-1.116-2.145-3.023v-.018c0-1.89.782-3.023 2.145-3.023 1.353 0 2.144 1.134 2.144 3.023v.018c0 1.907-.782 3.023-2.144 3.023Zm10.767 2.013c2.522 0 4.034-1.353 4.297-3.463l.01-.053h-2.374l-.017.036c-.229.966-.853 1.467-1.908 1.467-1.37 0-2.135-1.08-2.135-3.04v-.018c0-1.934.755-3.006 2.135-3.006 1.099 0 1.74.615 1.908 1.556l.008.017h2.391v-.026c-.228-2.162-1.749-3.56-4.315-3.56-3.033 0-4.738 1.837-4.738 5.019v.017c0 3.217 1.714 5.054 4.738 5.054Zm10.257 0c2.98 0 4.772-1.88 4.772-5.036v-.018c0-3.128-1.82-5.036-4.772-5.036-2.953 0-4.773 1.916-4.773 5.036v.018c0 3.146 1.793 5.036 4.773 5.036Zm0-2.013c-1.371 0-2.145-1.116-2.145-3.023v-.018c0-1.89.782-3.023 2.145-3.023 1.353 0 2.144 1.134 2.144 3.023v.018c0 1.907-.782 3.023-2.144 3.023ZM95.025 18h2.566V4.623h-2.566V18Z"
/>
</svg>
)
}

View File

@ -0,0 +1,173 @@
"use client";
import { Header } from "@/components/docs/Header";
import { Navigation } from "@/components/docs/Navigation";
import { Dialog, Transition } from "@headlessui/react";
import { motion } from "framer-motion";
import { usePathname, useSearchParams } from "next/navigation";
import {
createContext,
Fragment,
Suspense,
useContext,
useEffect,
useRef,
} from "react";
import { create } from "zustand";
function MenuIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg
viewBox="0 0 10 9"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="M.5 1h9M.5 8h9M.5 4.5h9" />
</svg>
);
}
function XIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg
viewBox="0 0 10 9"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="m1.5 1 7 7M8.5 1l-7 7" />
</svg>
);
}
const IsInsideMobileNavigationContext = createContext(false);
function MobileNavigationDialog({
isOpen,
close,
}: {
isOpen: boolean;
close: () => void;
}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const initialPathname = useRef(pathname).current;
const initialSearchParams = useRef(searchParams).current;
useEffect(() => {
if (pathname !== initialPathname || searchParams !== initialSearchParams) {
close();
}
}, [pathname, searchParams, close, initialPathname, initialSearchParams]);
function onClickDialog(event: React.MouseEvent<HTMLDivElement>) {
if (!(event.target instanceof HTMLElement)) {
return;
}
const link = event.target.closest("a");
if (
link &&
link.pathname + link.search + link.hash ===
window.location.pathname + window.location.search + window.location.hash
) {
close();
}
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
onClickCapture={onClickDialog}
onClose={close}
className="fixed inset-0 z-50 lg:hidden"
>
<Transition.Child
as={Fragment}
enter="duration-300 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-200 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 top-14 bg-zinc-400/20 backdrop-blur-sm dark:bg-black/40" />
</Transition.Child>
<Dialog.Panel>
<Transition.Child
as={Fragment}
enter="duration-300 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-200 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Header />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="duration-500 ease-in-out"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="duration-500 ease-in-out"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<motion.div
layoutScroll
className="fixed bottom-0 left-0 top-14 w-full overflow-y-auto bg-white px-4 pb-4 pt-6 shadow-lg shadow-zinc-900/10 ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 min-[416px]:max-w-sm sm:px-6 sm:pb-10"
>
<Navigation />
</motion.div>
</Transition.Child>
</Dialog.Panel>
</Dialog>
</Transition.Root>
);
}
export function useIsInsideMobileNavigation() {
return useContext(IsInsideMobileNavigationContext);
}
export const useMobileNavigationStore = create<{
isOpen: boolean;
open: () => void;
close: () => void;
toggle: () => void;
}>()((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}));
export function MobileNavigation() {
const isInsideMobileNavigation = useIsInsideMobileNavigation();
const { isOpen, toggle, close } = useMobileNavigationStore();
const ToggleIcon = isOpen ? XIcon : MenuIcon;
return (
<IsInsideMobileNavigationContext.Provider value={true}>
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label="Toggle navigation"
onClick={toggle}
>
<ToggleIcon className="w-2.5 stroke-zinc-900 dark:stroke-white" />
</button>
{!isInsideMobileNavigation && (
<Suspense fallback={null}>
<MobileNavigationDialog isOpen={isOpen} close={close} />
</Suspense>
)}
</IsInsideMobileNavigationContext.Provider>
);
}

View File

@ -0,0 +1,265 @@
"use client";
import { Button } from "@/components/docs/Button";
import { useIsInsideMobileNavigation } from "@/components/docs/MobileNavigation";
import { useSectionStore } from "@/components/docs/SectionProvider";
import { Tag } from "@/components/docs/Tag";
import { remToPx } from "@/lib/remToPx";
import clsx from "clsx";
import { AnimatePresence, motion, useIsPresent } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useRef } from "react";
interface NavGroup {
title: string;
links: Array<{
title: string;
href: string;
}>;
}
function useInitialValue<T>(value: T, condition = true) {
const initialValue = useRef(value).current;
return condition ? initialValue : value;
}
function TopLevelNavItem({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<li className="md:hidden">
<Link
href={href}
className="block py-1 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
>
{children}
</Link>
</li>
);
}
function NavLink({
href,
children,
tag,
active = false,
isAnchorLink = false,
}: {
href: string;
children: React.ReactNode;
tag?: string;
active?: boolean;
isAnchorLink?: boolean;
}) {
return (
<Link
href={href}
aria-current={active ? "page" : undefined}
className={clsx(
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
isAnchorLink ? "pl-7" : "pl-4",
active
? "text-zinc-900 dark:text-white"
: "text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
)}
>
<span className="truncate">{children}</span>
{tag && (
<Tag variant="small" color="zinc">
{tag}
</Tag>
)}
</Link>
);
}
function VisibleSectionHighlight({
group,
pathname,
}: {
group: NavGroup;
pathname: string;
}) {
const [sections, visibleSections] = useInitialValue(
[
useSectionStore((s) => s.sections),
useSectionStore((s) => s.visibleSections),
],
useIsInsideMobileNavigation()
);
const isPresent = useIsPresent();
const firstVisibleSectionIndex = Math.max(
0,
[{ id: "_top" }, ...sections].findIndex(
(section) => section.id === visibleSections[0]
)
);
const itemHeight = remToPx(2);
const height = isPresent
? Math.max(1, visibleSections.length) * itemHeight
: itemHeight;
const top =
group.links.findIndex((link) => link.href === pathname) * itemHeight +
firstVisibleSectionIndex * itemHeight;
return (
<motion.div
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 top-0 bg-zinc-800/2.5 will-change-transform dark:bg-white/2.5"
style={{ borderRadius: 8, height, top }}
/>
);
}
function ActivePageMarker({
group,
pathname,
}: {
group: NavGroup;
pathname: string;
}) {
const itemHeight = remToPx(2);
const offset = remToPx(0.25);
const activePageIndex = group.links.findIndex(
(link) => link.href === pathname
);
const top = offset + activePageIndex * itemHeight;
return (
<motion.div
layout
className="absolute left-2 h-6 w-px bg-emerald-500"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
style={{ top }}
/>
);
}
function NavigationGroup({
group,
className,
}: {
group: NavGroup;
className?: string;
}) {
// If this is the mobile navigation then we always render the initial
// state, so that the state does not change during the close animation.
// The state will still update when we re-open (re-render) the navigation.
const isInsideMobileNavigation = useIsInsideMobileNavigation();
const [pathname, sections] = useInitialValue(
[usePathname(), useSectionStore((s) => s.sections)],
isInsideMobileNavigation
);
const isActiveGroup =
group.links.findIndex((link) => link.href === pathname) !== -1;
return (
<li className={clsx("relative mt-6", className)}>
<motion.h2
layout="position"
className="text-xs font-semibold text-zinc-900 dark:text-white"
>
{group.title}
</motion.h2>
<div className="relative mt-3 pl-2">
<AnimatePresence initial={!isInsideMobileNavigation}>
{isActiveGroup && (
<VisibleSectionHighlight group={group} pathname={pathname} />
)}
</AnimatePresence>
<motion.div
layout
className="absolute inset-y-0 left-2 w-px bg-zinc-900/10 dark:bg-white/5"
/>
<AnimatePresence initial={false}>
{isActiveGroup && (
<ActivePageMarker group={group} pathname={pathname} />
)}
</AnimatePresence>
<ul role="list" className="border-l border-transparent">
{group.links.map((link) => (
<motion.li key={link.href} layout="position" className="relative">
<NavLink href={link.href} active={link.href === pathname}>
{link.title}
</NavLink>
<AnimatePresence mode="popLayout" initial={false}>
{link.href === pathname && sections.length > 0 && (
<motion.ul
role="list"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { delay: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15 },
}}
>
{sections.map((section) => (
<li key={section.id}>
<NavLink
href={`${link.href}#${section.id}`}
tag={section.tag}
isAnchorLink
>
{section.title}
</NavLink>
</li>
))}
</motion.ul>
)}
</AnimatePresence>
</motion.li>
))}
</ul>
</div>
</li>
);
}
export const navigation: Array<NavGroup> = [
{
title: "Guides",
links: [
{ title: "Introduction", href: "/docs" },
{ title: "Installation", href: "/docs/install" },
],
},
];
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {
return (
<nav {...props}>
<ul role="list">
<TopLevelNavItem href="/">API</TopLevelNavItem>
<TopLevelNavItem href="#">Documentation</TopLevelNavItem>
<TopLevelNavItem href="#">Support</TopLevelNavItem>
{navigation.map((group, groupIndex) => (
<NavigationGroup
key={group.title}
group={group}
className={groupIndex === 0 ? "md:mt-0" : ""}
/>
))}
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
<Button href="#" variant="filled" className="w-full">
Sign in
</Button>
</li>
</ul>
</nav>
);
}

View File

@ -0,0 +1,24 @@
import clsx from 'clsx'
export function Prose<T extends React.ElementType = 'div'>({
as,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'className'> & {
as?: T
className?: string
}) {
let Component = as ?? 'div'
return (
<Component
className={clsx(
className,
'prose dark:prose-invert',
// `html :where(& > *)` is used to select all direct children without an increase in specificity like you'd get from just `& > *`
'[html_:where(&>*)]:mx-auto [html_:where(&>*)]:max-w-2xl [html_:where(&>*)]:lg:mx-[calc(50%-min(50%,theme(maxWidth.lg)))] [html_:where(&>*)]:lg:max-w-3xl',
)}
{...props}
/>
)
}

View File

@ -0,0 +1,182 @@
"use client";
import { GridPattern } from "@/components/docs/GridPattern";
import { Heading } from "@/components/docs/Heading";
import {
type MotionValue,
motion,
useMotionTemplate,
useMotionValue,
} from "framer-motion";
import { Mail, MessageSquare, User, Users } from "lucide-react";
import Link from "next/link";
interface Resource {
href: string;
name: string;
description: string;
icon: React.ComponentType<{ className?: string }>;
pattern: Omit<
React.ComponentPropsWithoutRef<typeof GridPattern>,
"width" | "height" | "x"
>;
}
const resources: Array<Resource> = [
{
href: "/contacts",
name: "Contacts",
description:
"Learn about the contact model and how to create, retrieve, update, delete, and list contacts.",
icon: User,
pattern: {
y: 16,
squares: [
[0, 1],
[1, 3],
],
},
},
{
href: "/conversations",
name: "Conversations",
description:
"Learn about the conversation model and how to create, retrieve, update, delete, and list conversations.",
icon: MessageSquare,
pattern: {
y: -6,
squares: [
[-1, 2],
[1, 3],
],
},
},
{
href: "/messages",
name: "Messages",
description:
"Learn about the message model and how to create, retrieve, update, delete, and list messages.",
icon: Mail,
pattern: {
y: 32,
squares: [
[0, 2],
[1, 4],
],
},
},
{
href: "/groups",
name: "Groups",
description:
"Learn about the group model and how to create, retrieve, update, delete, and list groups.",
icon: Users,
pattern: {
y: 22,
squares: [[0, 1]],
},
},
];
function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) {
return (
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-zinc-900/5 ring-1 ring-zinc-900/25 backdrop-blur-[2px] transition duration-300 group-hover:bg-white/50 group-hover:ring-zinc-900/25 dark:bg-white/7.5 dark:ring-white/15 dark:group-hover:bg-emerald-300/10 dark:group-hover:ring-emerald-400">
<Icon className="h-5 w-5 fill-zinc-700/10 stroke-zinc-700 transition-colors duration-300 group-hover:stroke-zinc-900 dark:fill-white/10 dark:stroke-zinc-400 dark:group-hover:fill-emerald-300/10 dark:group-hover:stroke-emerald-400" />
</div>
);
}
function ResourcePattern({
mouseX,
mouseY,
...gridProps
}: Resource["pattern"] & {
mouseX: MotionValue<number>;
mouseY: MotionValue<number>;
}) {
const maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`;
const style = { maskImage, WebkitMaskImage: maskImage };
return (
<div className="pointer-events-none">
<div className="absolute inset-0 rounded-2xl transition duration-300 [mask-image:linear-gradient(white,transparent)] group-hover:opacity-50">
<GridPattern
width={72}
height={56}
x="50%"
className="absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-[-18deg] fill-black/[0.02] stroke-black/5 dark:fill-white/1 dark:stroke-white/2.5"
{...gridProps}
/>
</div>
<motion.div
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-[#D7EDEA] to-[#F4FBDF] opacity-0 transition duration-300 group-hover:opacity-100 dark:from-[#202D2E] dark:to-[#303428]"
style={style}
/>
<motion.div
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay transition duration-300 group-hover:opacity-100"
style={style}
>
<GridPattern
width={72}
height={56}
x="50%"
className="absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-[-18deg] fill-black/50 stroke-black/70 dark:fill-white/2.5 dark:stroke-white/10"
{...gridProps}
/>
</motion.div>
</div>
);
}
function Resource({ resource }: { resource: Resource }) {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
function onMouseMove({
currentTarget,
clientX,
clientY,
}: React.MouseEvent<HTMLDivElement>) {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top);
}
return (
<div
key={resource.href}
onMouseMove={onMouseMove}
className="group relative flex rounded-2xl bg-zinc-50 transition-shadow hover:shadow-md hover:shadow-zinc-900/5 dark:bg-white/2.5 dark:hover:shadow-black/5"
>
<ResourcePattern {...resource.pattern} mouseX={mouseX} mouseY={mouseY} />
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-zinc-900/7.5 group-hover:ring-zinc-900/10 dark:ring-white/10 dark:group-hover:ring-white/20" />
<div className="relative rounded-2xl px-4 pb-4 pt-16">
<ResourceIcon icon={resource.icon} />
<h3 className="mt-4 text-sm font-semibold leading-7 text-zinc-900 dark:text-white">
<Link href={resource.href}>
<span className="absolute inset-0 rounded-2xl" />
{resource.name}
</Link>
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{resource.description}
</p>
</div>
</div>
);
}
export function Resources() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="resources">
Resources
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-8 border-t border-zinc-900/5 pt-10 dark:border-white/5 sm:grid-cols-2 xl:grid-cols-4">
{resources.map((resource) => (
<Resource key={resource.href} resource={resource} />
))}
</div>
</div>
);
}

View File

@ -0,0 +1,504 @@
"use client";
import { navigation } from "@/components/docs/Navigation";
import { type Result } from "@/mdx/search.mjs";
import {
type AutocompleteApi,
createAutocomplete,
type AutocompleteState,
type AutocompleteCollection,
} from "@algolia/autocomplete-core";
import { Dialog, Transition } from "@headlessui/react";
import clsx from "clsx";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
forwardRef,
Fragment,
Suspense,
useCallback,
useEffect,
useId,
useRef,
useState,
} from "react";
import Highlighter from "react-highlight-words";
type EmptyObject = Record<string, never>;
type Autocomplete = AutocompleteApi<
Result,
React.SyntheticEvent,
React.MouseEvent,
React.KeyboardEvent
>;
function useAutocomplete({ close }: { close: () => void }) {
const id = useId();
const router = useRouter();
const [autocompleteState, setAutocompleteState] = useState<
AutocompleteState<Result> | EmptyObject
>({});
function navigate({ itemUrl }: { itemUrl?: string }) {
if (!itemUrl) {
return;
}
router.push(itemUrl);
if (
itemUrl ===
window.location.pathname + window.location.search + window.location.hash
) {
close();
}
}
const [autocomplete] = useState<Autocomplete>(() =>
createAutocomplete<
Result,
React.SyntheticEvent,
React.MouseEvent,
React.KeyboardEvent
>({
id,
placeholder: "Find something...",
defaultActiveItemId: 0,
onStateChange({ state }) {
setAutocompleteState(state);
},
shouldPanelOpen({ state }) {
return state.query !== "";
},
navigator: {
navigate,
},
getSources({ query }) {
return import("@/mdx/search.mjs").then(({ search }) => {
return [
{
sourceId: "documentation",
getItems() {
return search(query, { limit: 5 });
},
getItemUrl({ item }) {
return item.url;
},
onSelect: navigate,
},
];
});
},
})
);
return { autocomplete, autocompleteState };
}
function SearchIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12.01 12a4.25 4.25 0 1 0-6.02-6 4.25 4.25 0 0 0 6.02 6Zm0 0 3.24 3.25"
/>
</svg>
);
}
function NoResultsIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12.01 12a4.237 4.237 0 0 0 1.24-3c0-.62-.132-1.207-.37-1.738M12.01 12A4.237 4.237 0 0 1 9 13.25c-.635 0-1.237-.14-1.777-.388M12.01 12l3.24 3.25m-3.715-9.661a4.25 4.25 0 0 0-5.975 5.908M4.5 15.5l11-11"
/>
</svg>
);
}
function LoadingIcon(props: React.ComponentPropsWithoutRef<"svg">) {
const id = useId();
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" strokeLinejoin="round" />
<path
stroke={`url(#${id})`}
strokeLinecap="round"
strokeLinejoin="round"
d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5"
/>
<defs>
<linearGradient
id={id}
x1="13"
x2="9.5"
y1="9"
y2="15"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" />
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
}
function HighlightQuery({ text, query }: { text: string; query: string }) {
return (
<Highlighter
highlightClassName="underline bg-transparent text-emerald-500"
searchWords={[query]}
autoEscape={true}
textToHighlight={text}
/>
);
}
function SearchResult({
result,
resultIndex,
autocomplete,
collection,
query,
}: {
result: Result;
resultIndex: number;
autocomplete: Autocomplete;
collection: AutocompleteCollection<Result>;
query: string;
}) {
const id = useId();
const sectionTitle = navigation.find((section) =>
section.links.find((link) => link.href === result.url.split("#")[0])
)?.title;
const hierarchy = [sectionTitle, result.pageTitle].filter(
(x): x is string => typeof x === "string"
);
return (
<li
className={clsx(
"group block cursor-default px-4 py-3 aria-selected:bg-zinc-50 dark:aria-selected:bg-zinc-800/50",
resultIndex > 0 && "border-t border-zinc-100 dark:border-zinc-800"
)}
aria-labelledby={`${id}-hierarchy ${id}-title`}
{...autocomplete.getItemProps({
item: result,
source: collection.source,
})}
>
<div
id={`${id}-title`}
aria-hidden="true"
className="text-sm font-medium text-zinc-900 group-aria-selected:text-emerald-500 dark:text-white"
>
<HighlightQuery text={result.title} query={query} />
</div>
{hierarchy.length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
className="mt-1 truncate whitespace-nowrap text-2xs text-zinc-500"
>
{hierarchy.map((item, itemIndex, items) => (
<Fragment key={itemIndex}>
<HighlightQuery text={item} query={query} />
<span
className={
itemIndex === items.length - 1
? "sr-only"
: "mx-2 text-zinc-300 dark:text-zinc-700"
}
>
/
</span>
</Fragment>
))}
</div>
)}
</li>
);
}
function SearchResults({
autocomplete,
query,
collection,
}: {
autocomplete: Autocomplete;
query: string;
collection: AutocompleteCollection<Result>;
}) {
if (collection.items.length === 0) {
return (
<div className="p-6 text-center">
<NoResultsIcon className="mx-auto h-5 w-5 stroke-zinc-900 dark:stroke-zinc-600" />
<p className="mt-2 text-xs text-zinc-700 dark:text-zinc-400">
Nothing found for{" "}
<strong className="break-words font-semibold text-zinc-900 dark:text-white">
&lsquo;{query}&rsquo;
</strong>
. Please try again.
</p>
</div>
);
}
return (
<ul {...autocomplete.getListProps()}>
{collection.items.map((result, resultIndex) => (
<SearchResult
key={result.url}
result={result}
resultIndex={resultIndex}
autocomplete={autocomplete}
collection={collection}
query={query}
/>
))}
</ul>
);
}
const SearchInput = forwardRef<
React.ElementRef<"input">,
{
autocomplete: Autocomplete;
autocompleteState: AutocompleteState<Result> | EmptyObject;
onClose: () => void;
}
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
const inputProps = autocomplete.getInputProps({ inputElement: null });
return (
<div className="group relative flex h-12">
<SearchIcon className="pointer-events-none absolute left-3 top-0 h-full w-5 stroke-zinc-500" />
<input
ref={inputRef}
className={clsx(
"flex-auto appearance-none bg-transparent pl-10 text-zinc-900 outline-none placeholder:text-zinc-500 focus:w-full focus:flex-none dark:text-white sm:text-sm [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
autocompleteState.status === "stalled" ? "pr-11" : "pr-4"
)}
{...inputProps}
onKeyDown={(event) => {
if (
event.key === "Escape" &&
!autocompleteState.isOpen &&
autocompleteState.query === ""
) {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
onClose();
} else {
inputProps.onKeyDown(event);
}
}}
/>
{autocompleteState.status === "stalled" && (
<div className="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon className="h-5 w-5 animate-spin stroke-zinc-200 text-zinc-900 dark:stroke-zinc-800 dark:text-emerald-400" />
</div>
)}
</div>
);
});
function SearchDialog({
open,
setOpen,
className,
}: {
open: boolean;
setOpen: (open: boolean) => void;
className?: string;
}) {
const formRef = useRef<React.ElementRef<"form">>(null);
const panelRef = useRef<React.ElementRef<"div">>(null);
const inputRef = useRef<React.ElementRef<typeof SearchInput>>(null);
const { autocomplete, autocompleteState } = useAutocomplete({
close() {
setOpen(false);
},
});
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
setOpen(false);
}, [pathname, searchParams, setOpen]);
useEffect(() => {
if (open) {
return;
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setOpen(true);
}
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [open, setOpen]);
return (
<Transition.Root
show={open}
as={Fragment}
afterLeave={() => autocomplete.setQuery("")}
>
<Dialog
onClose={setOpen}
className={clsx("fixed inset-0 z-50", className)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-zinc-400/25 backdrop-blur-sm dark:bg-black/40" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="mx-auto transform-gpu overflow-hidden rounded-lg bg-zinc-50 shadow-xl ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 sm:max-w-xl">
<div {...autocomplete.getRootProps({})}>
<form
ref={formRef}
{...autocomplete.getFormProps({
inputElement: inputRef.current,
})}
>
<SearchInput
ref={inputRef}
autocomplete={autocomplete}
autocompleteState={autocompleteState}
onClose={() => setOpen(false)}
/>
<div
ref={panelRef}
className="border-t border-zinc-200 bg-white empty:hidden dark:border-zinc-100/5 dark:bg-white/2.5"
{...autocomplete.getPanelProps({})}
>
{autocompleteState.isOpen && (
<SearchResults
autocomplete={autocomplete}
query={autocompleteState.query}
collection={autocompleteState.collections[0]}
/>
)}
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
}
function useSearchProps() {
const buttonRef = useRef<React.ElementRef<"button">>(null);
const [open, setOpen] = useState(false);
return {
buttonProps: {
ref: buttonRef,
onClick() {
setOpen(true);
},
},
dialogProps: {
open,
setOpen: useCallback(
(open: boolean) => {
const { width = 0, height = 0 } =
buttonRef.current?.getBoundingClientRect() ?? {};
if (!open || (width !== 0 && height !== 0)) {
setOpen(open);
}
},
[setOpen]
),
},
};
}
export function Search() {
const [modifierKey, setModifierKey] = useState<string>();
const { buttonProps, dialogProps } = useSearchProps();
useEffect(() => {
setModifierKey(
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl "
);
}, []);
return (
<div className="hidden lg:block lg:max-w-md lg:flex-auto">
<button
type="button"
className="hidden h-8 w-full items-center gap-2 rounded-full bg-white pl-2 pr-3 text-sm text-zinc-500 ring-1 ring-zinc-900/10 transition hover:ring-zinc-900/20 ui-not-focus-visible:outline-none dark:bg-white/5 dark:text-zinc-400 dark:ring-inset dark:ring-white/10 dark:hover:ring-white/20 lg:flex"
{...buttonProps}
>
<SearchIcon className="h-5 w-5 stroke-current" />
Find something...
<kbd className="ml-auto text-2xs text-zinc-400 dark:text-zinc-500">
<kbd className="font-sans">{modifierKey}</kbd>
<kbd className="font-sans">K</kbd>
</kbd>
</button>
<Suspense fallback={null}>
<SearchDialog className="hidden lg:block" {...dialogProps} />
</Suspense>
</div>
);
}
export function MobileSearch() {
const { buttonProps, dialogProps } = useSearchProps();
return (
<div className="contents lg:hidden">
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 ui-not-focus-visible:outline-none dark:hover:bg-white/5 lg:hidden"
aria-label="Find something..."
{...buttonProps}
>
<SearchIcon className="h-5 w-5 stroke-zinc-900 dark:stroke-white" />
</button>
<Suspense fallback={null}>
<SearchDialog className="lg:hidden" {...dialogProps} />
</Suspense>
</div>
);
}

View File

@ -0,0 +1,155 @@
"use client";
import { remToPx } from "@/lib/remToPx";
import {
createContext,
useContext,
useEffect,
useLayoutEffect,
useState,
} from "react";
import { type StoreApi, createStore, useStore } from "zustand";
export interface Section {
id: string;
title: string;
offsetRem?: number;
tag?: string;
headingRef?: React.RefObject<HTMLHeadingElement>;
}
interface SectionState {
sections: Array<Section>;
visibleSections: Array<string>;
setVisibleSections: (visibleSections: Array<string>) => void;
registerHeading: ({
id,
ref,
offsetRem,
}: {
id: string;
ref: React.RefObject<HTMLHeadingElement>;
offsetRem: number;
}) => void;
}
function createSectionStore(sections: Array<Section>) {
return createStore<SectionState>()((set) => ({
sections,
visibleSections: [],
setVisibleSections: (visibleSections) =>
set((state) =>
state.visibleSections.join() === visibleSections.join()
? {}
: { visibleSections }
),
registerHeading: ({ id, ref, offsetRem }) =>
set((state) => {
return {
sections: state.sections.map((section) => {
if (section.id === id) {
return {
...section,
headingRef: ref,
offsetRem,
};
}
return section;
}),
};
}),
}));
}
function useVisibleSections(sectionStore: StoreApi<SectionState>) {
const setVisibleSections = useStore(
sectionStore,
(s) => s.setVisibleSections
);
const sections = useStore(sectionStore, (s) => s.sections);
useEffect(() => {
function checkVisibleSections() {
const { innerHeight, scrollY } = window;
const newVisibleSections = [];
for (
let sectionIndex = 0;
sectionIndex < sections.length;
sectionIndex++
) {
const { id, headingRef, offsetRem = 0 } = sections[sectionIndex];
if (!headingRef?.current) {
continue;
}
const offset = remToPx(offsetRem);
const top = headingRef.current.getBoundingClientRect().top + scrollY;
if (sectionIndex === 0 && top - offset > scrollY) {
newVisibleSections.push("_top");
}
const nextSection = sections[sectionIndex + 1];
const bottom =
(nextSection?.headingRef?.current?.getBoundingClientRect().top ??
Infinity) +
scrollY -
remToPx(nextSection?.offsetRem ?? 0);
if (
(top > scrollY && top < scrollY + innerHeight) ||
(bottom > scrollY && bottom < scrollY + innerHeight) ||
(top <= scrollY && bottom >= scrollY + innerHeight)
) {
newVisibleSections.push(id);
}
}
setVisibleSections(newVisibleSections);
}
const raf = window.requestAnimationFrame(() => checkVisibleSections());
window.addEventListener("scroll", checkVisibleSections, { passive: true });
window.addEventListener("resize", checkVisibleSections);
return () => {
window.cancelAnimationFrame(raf);
window.removeEventListener("scroll", checkVisibleSections);
window.removeEventListener("resize", checkVisibleSections);
};
}, [setVisibleSections, sections]);
}
const SectionStoreContext = createContext<StoreApi<SectionState> | null>(null);
const useIsomorphicLayoutEffect =
typeof window === "undefined" ? useEffect : useLayoutEffect;
export function SectionProvider({
sections,
children,
}: {
sections: Array<Section>;
children: React.ReactNode;
}) {
const [sectionStore] = useState(() => createSectionStore(sections));
useVisibleSections(sectionStore);
useIsomorphicLayoutEffect(() => {
sectionStore.setState({ sections });
}, [sectionStore, sections]);
return (
<SectionStoreContext.Provider value={sectionStore}>
{children}
</SectionStoreContext.Provider>
);
}
export function useSectionStore<T>(selector: (state: SectionState) => T) {
const store = useContext(SectionStoreContext);
return useStore(store!, selector);
}

View File

@ -0,0 +1,63 @@
import clsx from 'clsx'
const variantStyles = {
small: '',
medium: 'rounded-lg px-1.5 ring-1 ring-inset',
}
const colorStyles = {
emerald: {
small: 'text-emerald-500 dark:text-emerald-400',
medium:
'ring-emerald-300 dark:ring-emerald-400/30 bg-emerald-400/10 text-emerald-500 dark:text-emerald-400',
},
sky: {
small: 'text-sky-500',
medium:
'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400',
},
amber: {
small: 'text-amber-500',
medium:
'ring-amber-300 bg-amber-400/10 text-amber-500 dark:ring-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400',
},
rose: {
small: 'text-red-500 dark:text-rose-500',
medium:
'ring-rose-200 bg-rose-50 text-red-500 dark:ring-rose-500/20 dark:bg-rose-400/10 dark:text-rose-400',
},
zinc: {
small: 'text-zinc-400 dark:text-zinc-500',
medium:
'ring-zinc-200 bg-zinc-50 text-zinc-500 dark:ring-zinc-500/20 dark:bg-zinc-400/10 dark:text-zinc-400',
},
}
const valueColorMap = {
GET: 'emerald',
POST: 'sky',
PUT: 'amber',
DELETE: 'rose',
} as Record<string, keyof typeof colorStyles>
export function Tag({
children,
variant = 'medium',
color = valueColorMap[children] ?? 'emerald',
}: {
children: keyof typeof valueColorMap & (string | {})
variant?: keyof typeof variantStyles
color?: keyof typeof colorStyles
}) {
return (
<span
className={clsx(
'font-mono text-[0.625rem] font-semibold leading-6',
variantStyles[variant],
colorStyles[color][variant],
)}
>
{children}
</span>
)
}

View File

@ -0,0 +1,44 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
function SunIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path d="M12.5 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z" />
<path
strokeLinecap="round"
d="M10 5.5v-1M13.182 6.818l.707-.707M14.5 10h1M13.182 13.182l.707.707M10 15.5v-1M6.11 13.889l.708-.707M4.5 10h1M6.11 6.111l.708.707"
/>
</svg>
)
}
function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path d="M15.224 11.724a5.5 5.5 0 0 1-6.949-6.949 5.5 5.5 0 1 0 6.949 6.949Z" />
</svg>
)
}
export function ThemeToggle() {
let { resolvedTheme, setTheme } = useTheme()
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
let [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return (
<button
type="button"
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label={mounted ? `Switch to ${otherTheme} theme` : 'Toggle theme'}
onClick={() => setTheme(otherTheme)}
>
<SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" />
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
</button>
)
}

View File

@ -0,0 +1,144 @@
import { Feedback } from "@/components/docs/Feedback";
import { Heading } from "@/components/docs/Heading";
import { Prose } from "@/components/docs/Prose";
import { cn } from "@/lib/utils";
import clsx from "clsx";
import Link from "next/link";
export const a = Link;
export { Button } from "@/components/docs/Button";
export { CodeGroup, Code as code, Pre as pre } from "@/components/docs/Code";
export function wrapper({ children }: { children: React.ReactNode }) {
return (
<article className="flex h-full flex-col pb-10 pt-16">
<Prose className="flex-auto">{children}</Prose>
<footer className="mx-auto mt-16 w-full max-w-2xl lg:max-w-5xl">
<Feedback />
</footer>
</article>
);
}
export const h2 = function H2(
props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, "level">
) {
return <Heading level={2} {...props} />;
};
function InfoIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
<circle cx="8" cy="8" r="8" strokeWidth="0" />
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M6.75 7.75h1.5v3.5"
/>
<circle cx="8" cy="4" r=".5" fill="none" />
</svg>
);
}
export function Image({
className,
...props
}: React.ComponentPropsWithoutRef<"img">) {
return (
<img
{...props}
className={cn(
"w-full rounded-2xl ring-1 ring-stone-200 p-2 bg-stone-100 object-contain",
className
)}
loading="lazy"
alt=""
/>
);
}
export function Note({ children }: { children: React.ReactNode }) {
return (
<div className="my-6 flex gap-2.5 rounded-2xl border border-emerald-500/20 bg-emerald-50/50 p-4 leading-6 text-emerald-900 dark:border-emerald-500/30 dark:bg-emerald-500/5 dark:text-emerald-200 dark:[--tw-prose-links-hover:theme(colors.emerald.300)] dark:[--tw-prose-links:theme(colors.white)]">
<InfoIcon className="mt-1 h-4 w-4 flex-none fill-emerald-500 stroke-white dark:fill-emerald-200/20 dark:stroke-emerald-200" />
<div className="[&>:first-child]:mt-0 [&>:last-child]:mb-0">
{children}
</div>
</div>
);
}
export function Row({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
{children}
</div>
);
}
export function Col({
children,
sticky = false,
}: {
children: React.ReactNode;
sticky?: boolean;
}) {
return (
<div
className={clsx(
"[&>:first-child]:mt-0 [&>:last-child]:mb-0",
sticky && "xl:sticky xl:top-24"
)}
>
{children}
</div>
);
}
export function Properties({ children }: { children: React.ReactNode }) {
return (
<div className="my-6">
<ul
role="list"
className="m-0 max-w-[calc(theme(maxWidth.lg)-theme(spacing.8))] list-none divide-y divide-zinc-900/5 p-0 dark:divide-white/5"
>
{children}
</ul>
</div>
);
}
export function Property({
name,
children,
type,
}: {
name: string;
children: React.ReactNode;
type?: string;
}) {
return (
<li className="m-0 px-0 py-4 first:pt-0 last:pb-0">
<dl className="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dt className="sr-only">Name</dt>
<dd>
<code>{name}</code>
</dd>
{type && (
<>
<dt className="sr-only">Type</dt>
<dd className="font-mono text-xs text-zinc-400 dark:text-zinc-500">
{type}
</dd>
</>
)}
<dt className="sr-only">Description</dt>
<dd className="w-full flex-none [&>:first-child]:mt-0 [&>:last-child]:mb-0">
{children}
</dd>
</dl>
</li>
);
}

View File

@ -1,11 +1,10 @@
"use client" "use client";
import * as React from "react" import { cn } from "@/lib/utils";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "@/lib/utils" const Tabs = TabsPrimitive.Root;
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
@ -19,8 +18,8 @@ const TabsList = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
@ -29,13 +28,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className className
)} )}
{...props} {...props}
/> />
)) ));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
@ -49,7 +48,7 @@ const TabsContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
TabsContent.displayName = TabsPrimitive.Content.displayName TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

8
web/src/lib/remToPx.ts Normal file
View File

@ -0,0 +1,8 @@
export function remToPx(remValue: number) {
let rootFontSize =
typeof window === 'undefined'
? 16
: parseFloat(window.getComputedStyle(document.documentElement).fontSize)
return remValue * rootFontSize
}

3
web/src/mdx/recma.mjs Normal file
View File

@ -0,0 +1,3 @@
import { mdxAnnotations } from "mdx-annotations";
export const recmaPlugins = [mdxAnnotations.recma];

124
web/src/mdx/rehype.mjs Normal file
View File

@ -0,0 +1,124 @@
import { slugifyWithCounter } from "@sindresorhus/slugify";
import * as acorn from "acorn";
import { toString } from "mdast-util-to-string";
import { mdxAnnotations } from "mdx-annotations";
import shiki from "shiki";
import { visit } from "unist-util-visit";
function rehypeParseCodeBlocks() {
return (tree) => {
visit(tree, "element", (node, _nodeIndex, parentNode) => {
if (node.tagName === "code" && node.properties.className) {
parentNode.properties.language = node.properties.className[0]?.replace(
/^language-/,
""
);
}
});
};
}
let highlighter;
function rehypeShiki() {
return async (tree) => {
highlighter =
highlighter ?? (await shiki.getHighlighter({ theme: "css-variables" }));
visit(tree, "element", (node) => {
if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
let codeNode = node.children[0];
let textNode = codeNode.children[0];
node.properties.code = textNode.value;
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(
textNode.value,
node.properties.language
);
textNode.value = shiki.renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `<span>${children}</span>`,
},
});
}
}
});
};
}
function rehypeSlugify() {
return (tree) => {
let slugify = slugifyWithCounter();
visit(tree, "element", (node) => {
if (node.tagName === "h2" && !node.properties.id) {
node.properties.id = slugify(toString(node));
}
});
};
}
function rehypeAddMDXExports(getExports) {
return (tree) => {
let exports = Object.entries(getExports(tree));
for (let [name, value] of exports) {
for (let node of tree.children) {
if (
node.type === "mdxjsEsm" &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
) {
return;
}
}
let exportStr = `export const ${name} = ${value}`;
tree.children.push({
type: "mdxjsEsm",
value: exportStr,
data: {
estree: acorn.parse(exportStr, {
sourceType: "module",
ecmaVersion: "latest",
}),
},
});
}
};
}
function getSections(node) {
let sections = [];
for (let child of node.children ?? []) {
if (child.type === "element" && child.tagName === "h2") {
sections.push(`{
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
...${child.properties.annotation}
}`);
} else if (child.children) {
sections.push(...getSections(child));
}
}
return sections;
}
export const rehypePlugins = [
mdxAnnotations.rehype,
rehypeParseCodeBlocks,
rehypeShiki,
rehypeSlugify,
[
rehypeAddMDXExports,
(tree) => ({
sections: `[${getSections(tree).join()}]`,
}),
],
];

4
web/src/mdx/remark.mjs Normal file
View File

@ -0,0 +1,4 @@
import { mdxAnnotations } from 'mdx-annotations'
import remarkGfm from 'remark-gfm'
export const remarkPlugins = [mdxAnnotations.remark, remarkGfm]

137
web/src/mdx/search.mjs Normal file
View File

@ -0,0 +1,137 @@
import { slugifyWithCounter } from "@sindresorhus/slugify";
import glob from "fast-glob";
import * as fs from "fs";
import { toString } from "mdast-util-to-string";
import * as path from "path";
import { remark } from "remark";
import remarkMdx from "remark-mdx";
import { createLoader } from "simple-functional-loader";
import { filter } from "unist-util-filter";
import { SKIP, visit } from "unist-util-visit";
import * as url from "url";
const __filename = url.fileURLToPath(import.meta.url);
const processor = remark().use(remarkMdx).use(extractSections);
const slugify = slugifyWithCounter();
function isObjectExpression(node) {
return (
node.type === "mdxTextExpression" &&
node.data?.estree?.body?.[0]?.expression?.type === "ObjectExpression"
);
}
function excludeObjectExpressions(tree) {
return filter(tree, (node) => !isObjectExpression(node));
}
function extractSections() {
return (tree, { sections }) => {
slugify.reset();
visit(tree, (node) => {
if (node.type === "heading" || node.type === "paragraph") {
let content = toString(excludeObjectExpressions(node));
if (node.type === "heading" && node.depth <= 2) {
let hash = node.depth === 1 ? null : slugify(content);
sections.push([content, hash, []]);
} else {
sections.at(-1)?.[2].push(content);
}
return SKIP;
}
});
};
}
export default function (nextConfig = {}) {
let cache = new Map();
return Object.assign({}, nextConfig, {
webpack(config, options) {
config.module.rules.push({
test: __filename,
use: [
createLoader(function () {
let appDir = path.resolve("./src/app/(docs)/docs");
this.addContextDependency(appDir);
let files = glob.sync("**/*.mdx", { cwd: appDir });
let data = files.map((file) => {
let url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`;
let mdx = fs.readFileSync(path.join(appDir, file), "utf8");
let sections = [];
if (cache.get(file)?.[0] === mdx) {
sections = cache.get(file)[1];
} else {
let vfile = { value: mdx, sections };
processor.runSync(processor.parse(vfile), vfile);
cache.set(file, [mdx, sections]);
}
url = `/docs/${url}`;
return { url, sections };
});
// When this file is imported within the application
// the following module is loaded:
return `
import FlexSearch from 'flexsearch'
let sectionIndex = new FlexSearch.Document({
tokenize: 'full',
document: {
id: 'url',
index: 'content',
store: ['title', 'pageTitle'],
},
context: {
resolution: 9,
depth: 2,
bidirectional: true
}
})
let data = ${JSON.stringify(data)}
for (let { url, sections } of data) {
for (let [title, hash, content] of sections) {
sectionIndex.add({
url: url + (hash ? ('#' + hash) : ''),
title,
content: [title, ...content].join('\\n'),
pageTitle: hash ? sections[0][0] : undefined,
})
}
}
export function search(query, options = {}) {
let result = sectionIndex.search(query, {
...options,
enrich: true,
})
if (result.length === 0) {
return []
}
return result[0].result.map((item) => ({
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,
}))
}
`;
}),
],
});
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options);
}
return config;
},
});
}

View File

@ -5,7 +5,7 @@ import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({ export default authMiddleware({
// debug: true, // debug: true,
publicRoutes: ['/',"/api/(.*)"], publicRoutes: ["/", "/api/(.*)", "/docs(.*)"],
// publicRoutes: ["/", "/(.*)"], // publicRoutes: ["/", "/(.*)"],
async afterAuth(auth, req, evt) { async afterAuth(auth, req, evt) {
// redirect them to organization selection page // redirect them to organization selection page

View File

@ -1,14 +1,29 @@
import typographyStyles from "./typography";
import headlessuiPlugin from "@headlessui/tailwindcss";
import typographyPlugin from "@tailwindcss/typography";
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
const config: Config = { const config: Config = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: ["./src/**/*.{js,mjs,jsx,ts,tsx,mdx}"],
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
theme: { theme: {
fontSize: {
"2xs": ["0.75rem", { lineHeight: "1.25rem" }],
xs: ["0.8125rem", { lineHeight: "1.5rem" }],
sm: ["0.875rem", { lineHeight: "1.5rem" }],
base: ["1rem", { lineHeight: "1.75rem" }],
lg: ["1.125rem", { lineHeight: "1.75rem" }],
xl: ["1.25rem", { lineHeight: "1.75rem" }],
"2xl": ["1.5rem", { lineHeight: "2rem" }],
"3xl": ["1.875rem", { lineHeight: "2.25rem" }],
"4xl": ["2.25rem", { lineHeight: "2.5rem" }],
"5xl": ["3rem", { lineHeight: "1" }],
"6xl": ["3.75rem", { lineHeight: "1" }],
"7xl": ["4.5rem", { lineHeight: "1" }],
"8xl": ["6rem", { lineHeight: "1" }],
"9xl": ["8rem", { lineHeight: "1" }],
},
typography: typographyStyles,
container: { container: {
center: true, center: true,
padding: "2rem", padding: "2rem",
@ -66,22 +81,37 @@ const config: Config = {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" }, to: { height: "0" },
}, },
'background-shine': { "background-shine": {
from: { from: {
backgroundPosition: '0 0', backgroundPosition: "0 0",
}, },
to: { to: {
backgroundPosition: '-200% 0', backgroundPosition: "-200% 0",
}, },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
'background-shine': 'background-shine 2s linear infinite', "background-shine": "background-shine 2s linear infinite",
},
boxShadow: {
glow: "0 0 4px rgb(0 0 0 / 0.1)",
},
maxWidth: {
lg: "33rem",
"2xl": "40rem",
"3xl": "50rem",
"5xl": "66rem",
},
opacity: {
1: "0.01",
2.5: "0.025",
7.5: "0.075",
15: "0.15",
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate"), typographyPlugin, headlessuiPlugin],
}; };
export default config; export default config;

11
web/types.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { type SearchOptions } from 'flexsearch'
declare module '@/mdx/search.mjs' {
export type Result = {
url: string
title: string
pageTitle?: string
}
export function search(query: string, options?: SearchOptions): Array<Result>
}

353
web/typography.ts Normal file
View File

@ -0,0 +1,353 @@
import { type PluginUtils } from "tailwindcss/types/config";
export default function typographyStyles({ theme }: PluginUtils) {
return {
DEFAULT: {
css: {
"--tw-prose-body": theme("colors.zinc.700"),
"--tw-prose-headings": theme("colors.zinc.900"),
"--tw-prose-links": theme("colors.emerald.500"),
"--tw-prose-links-hover": theme("colors.emerald.600"),
"--tw-prose-links-underline": theme("colors.emerald.500 / 0.3"),
"--tw-prose-bold": theme("colors.zinc.900"),
"--tw-prose-counters": theme("colors.zinc.500"),
"--tw-prose-bullets": theme("colors.zinc.300"),
"--tw-prose-hr": theme("colors.zinc.900 / 0.05"),
"--tw-prose-quotes": theme("colors.zinc.900"),
"--tw-prose-quote-borders": theme("colors.zinc.200"),
"--tw-prose-captions": theme("colors.zinc.500"),
"--tw-prose-code": theme("colors.zinc.900"),
"--tw-prose-code-bg": theme("colors.zinc.100"),
"--tw-prose-code-ring": theme("colors.zinc.300"),
"--tw-prose-th-borders": theme("colors.zinc.300"),
"--tw-prose-td-borders": theme("colors.zinc.200"),
"--tw-prose-invert-body": theme("colors.zinc.400"),
"--tw-prose-invert-headings": theme("colors.white"),
"--tw-prose-invert-links": theme("colors.emerald.400"),
"--tw-prose-invert-links-hover": theme("colors.emerald.500"),
"--tw-prose-invert-links-underline": theme("colors.emerald.500 / 0.3"),
"--tw-prose-invert-bold": theme("colors.white"),
"--tw-prose-invert-counters": theme("colors.zinc.400"),
"--tw-prose-invert-bullets": theme("colors.zinc.600"),
"--tw-prose-invert-hr": theme("colors.white / 0.05"),
"--tw-prose-invert-quotes": theme("colors.zinc.100"),
"--tw-prose-invert-quote-borders": theme("colors.zinc.700"),
"--tw-prose-invert-captions": theme("colors.zinc.400"),
"--tw-prose-invert-code": theme("colors.white"),
"--tw-prose-invert-code-bg": theme("colors.zinc.700 / 0.15"),
"--tw-prose-invert-code-ring": theme("colors.white / 0.1"),
"--tw-prose-invert-th-borders": theme("colors.zinc.600"),
"--tw-prose-invert-td-borders": theme("colors.zinc.700"),
// Base
color: "var(--tw-prose-body)",
fontSize: theme("fontSize.sm")[0],
lineHeight: theme("lineHeight.7"),
// Text
p: {
marginTop: theme("spacing.6"),
marginBottom: theme("spacing.6"),
},
'[class~="lead"]': {
fontSize: theme("fontSize.base")[0],
...theme("fontSize.base")[1],
},
// Lists
ol: {
listStyleType: "decimal",
marginTop: theme("spacing.5"),
marginBottom: theme("spacing.5"),
paddingLeft: "1.625rem",
},
'ol[type="A"]': {
listStyleType: "upper-alpha",
},
'ol[type="a"]': {
listStyleType: "lower-alpha",
},
'ol[type="A" s]': {
listStyleType: "upper-alpha",
},
'ol[type="a" s]': {
listStyleType: "lower-alpha",
},
'ol[type="I"]': {
listStyleType: "upper-roman",
},
'ol[type="i"]': {
listStyleType: "lower-roman",
},
'ol[type="I" s]': {
listStyleType: "upper-roman",
},
'ol[type="i" s]': {
listStyleType: "lower-roman",
},
'ol[type="1"]': {
listStyleType: "decimal",
},
ul: {
listStyleType: "disc",
marginTop: theme("spacing.5"),
marginBottom: theme("spacing.5"),
paddingLeft: "1.625rem",
},
li: {
marginTop: theme("spacing.2"),
marginBottom: theme("spacing.2"),
},
":is(ol, ul) > li": {
paddingLeft: theme("spacing[1.5]"),
},
"ol > li::marker": {
fontWeight: "400",
color: "var(--tw-prose-counters)",
},
"ul > li::marker": {
color: "var(--tw-prose-bullets)",
},
"> ul > li p": {
marginTop: theme("spacing.3"),
marginBottom: theme("spacing.3"),
},
"> ul > li > *:first-child": {
marginTop: theme("spacing.5"),
},
"> ul > li > *:last-child": {
marginBottom: theme("spacing.5"),
},
"> ol > li > *:first-child": {
marginTop: theme("spacing.5"),
},
"> ol > li > *:last-child": {
marginBottom: theme("spacing.5"),
},
"ul ul, ul ol, ol ul, ol ol": {
marginTop: theme("spacing.3"),
marginBottom: theme("spacing.3"),
},
// Horizontal rules
hr: {
borderColor: "var(--tw-prose-hr)",
borderTopWidth: 1,
marginTop: theme("spacing.16"),
marginBottom: theme("spacing.16"),
maxWidth: "none",
marginLeft: `calc(-1 * ${theme("spacing.4")})`,
marginRight: `calc(-1 * ${theme("spacing.4")})`,
"@screen sm": {
marginLeft: `calc(-1 * ${theme("spacing.6")})`,
marginRight: `calc(-1 * ${theme("spacing.6")})`,
},
"@screen lg": {
marginLeft: `calc(-1 * ${theme("spacing.8")})`,
marginRight: `calc(-1 * ${theme("spacing.8")})`,
},
},
// Quotes
blockquote: {
fontWeight: "500",
fontStyle: "italic",
color: "var(--tw-prose-quotes)",
borderLeftWidth: "0.25rem",
borderLeftColor: "var(--tw-prose-quote-borders)",
quotes: '"\\201C""\\201D""\\2018""\\2019"',
marginTop: theme("spacing.8"),
marginBottom: theme("spacing.8"),
paddingLeft: theme("spacing.5"),
},
"blockquote p:first-of-type::before": {
content: "open-quote",
},
"blockquote p:last-of-type::after": {
content: "close-quote",
},
// Headings
h1: {
color: "var(--tw-prose-headings)",
fontWeight: "700",
fontSize: theme("fontSize.2xl")[0],
...theme("fontSize.2xl")[1],
marginBottom: theme("spacing.2"),
},
h2: {
color: "var(--tw-prose-headings)",
fontWeight: "600",
fontSize: theme("fontSize.lg")[0],
...theme("fontSize.lg")[1],
marginTop: theme("spacing.16"),
marginBottom: theme("spacing.2"),
},
h3: {
color: "var(--tw-prose-headings)",
fontSize: theme("fontSize.base")[0],
...theme("fontSize.base")[1],
fontWeight: "600",
marginTop: theme("spacing.10"),
marginBottom: theme("spacing.2"),
},
// Media
// img: {
// marginTop: theme("spacing.2"),
// marginBottom: theme("spacing.2"),
// },
"video, figure": {
marginTop: theme("spacing.8"),
marginBottom: theme("spacing.8"),
},
"figure > *": {
marginTop: "0",
marginBottom: "0",
},
figcaption: {
color: "var(--tw-prose-captions)",
fontSize: theme("fontSize.xs")[0],
...theme("fontSize.xs")[1],
marginTop: theme("spacing.2"),
},
// Tables
table: {
width: "100%",
tableLayout: "auto",
textAlign: "left",
marginTop: theme("spacing.8"),
marginBottom: theme("spacing.8"),
lineHeight: theme("lineHeight.6"),
},
thead: {
borderBottomWidth: "1px",
borderBottomColor: "var(--tw-prose-th-borders)",
},
"thead th": {
color: "var(--tw-prose-headings)",
fontWeight: "600",
verticalAlign: "bottom",
paddingRight: theme("spacing.2"),
paddingBottom: theme("spacing.2"),
paddingLeft: theme("spacing.2"),
},
"thead th:first-child": {
paddingLeft: "0",
},
"thead th:last-child": {
paddingRight: "0",
},
"tbody tr": {
borderBottomWidth: "1px",
borderBottomColor: "var(--tw-prose-td-borders)",
},
"tbody tr:last-child": {
borderBottomWidth: "0",
},
"tbody td": {
verticalAlign: "baseline",
},
tfoot: {
borderTopWidth: "1px",
borderTopColor: "var(--tw-prose-th-borders)",
},
"tfoot td": {
verticalAlign: "top",
},
":is(tbody, tfoot) td": {
paddingTop: theme("spacing.2"),
paddingRight: theme("spacing.2"),
paddingBottom: theme("spacing.2"),
paddingLeft: theme("spacing.2"),
},
":is(tbody, tfoot) td:first-child": {
paddingLeft: "0",
},
":is(tbody, tfoot) td:last-child": {
paddingRight: "0",
},
// Inline elements
a: {
color: "var(--tw-prose-links)",
textDecoration: "underline transparent",
fontWeight: "500",
transitionProperty: "color, text-decoration-color",
transitionDuration: theme("transitionDuration.DEFAULT"),
transitionTimingFunction: theme("transitionTimingFunction.DEFAULT"),
"&:hover": {
color: "var(--tw-prose-links-hover)",
textDecorationColor: "var(--tw-prose-links-underline)",
},
},
":is(h1, h2, h3) a": {
fontWeight: "inherit",
},
strong: {
color: "var(--tw-prose-bold)",
fontWeight: "600",
},
":is(a, blockquote, thead th) strong": {
color: "inherit",
},
code: {
color: "var(--tw-prose-code)",
borderRadius: theme("borderRadius.lg"),
paddingTop: theme("padding.1"),
paddingRight: theme("padding[1.5]"),
paddingBottom: theme("padding.1"),
paddingLeft: theme("padding[1.5]"),
boxShadow: "inset 0 0 0 1px var(--tw-prose-code-ring)",
backgroundColor: "var(--tw-prose-code-bg)",
fontSize: theme("fontSize.2xs"),
},
":is(a, h1, h2, h3, blockquote, thead th) code": {
color: "inherit",
},
"h2 code": {
fontSize: theme("fontSize.base")[0],
fontWeight: "inherit",
},
"h3 code": {
fontSize: theme("fontSize.sm")[0],
fontWeight: "inherit",
},
// Overrides
":is(h1, h2, h3) + *": {
marginTop: "0",
},
"> :first-child": {
marginTop: "0 !important",
},
"> :last-child": {
marginBottom: "0 !important",
},
},
},
invert: {
css: {
"--tw-prose-body": "var(--tw-prose-invert-body)",
"--tw-prose-headings": "var(--tw-prose-invert-headings)",
"--tw-prose-links": "var(--tw-prose-invert-links)",
"--tw-prose-links-hover": "var(--tw-prose-invert-links-hover)",
"--tw-prose-links-underline": "var(--tw-prose-invert-links-underline)",
"--tw-prose-bold": "var(--tw-prose-invert-bold)",
"--tw-prose-counters": "var(--tw-prose-invert-counters)",
"--tw-prose-bullets": "var(--tw-prose-invert-bullets)",
"--tw-prose-hr": "var(--tw-prose-invert-hr)",
"--tw-prose-quotes": "var(--tw-prose-invert-quotes)",
"--tw-prose-quote-borders": "var(--tw-prose-invert-quote-borders)",
"--tw-prose-captions": "var(--tw-prose-invert-captions)",
"--tw-prose-code": "var(--tw-prose-invert-code)",
"--tw-prose-code-bg": "var(--tw-prose-invert-code-bg)",
"--tw-prose-code-ring": "var(--tw-prose-invert-code-ring)",
"--tw-prose-th-borders": "var(--tw-prose-invert-th-borders)",
"--tw-prose-td-borders": "var(--tw-prose-invert-td-borders)",
},
},
};
}