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
+1 -1
View File
@@ -96,7 +96,7 @@ export function MachineSelect({
setMachine(v);
}}
>
<SelectTrigger className="w-[180px]">
<SelectTrigger className="w-[180px] text-start">
<SelectValue placeholder="Select a machine" />
</SelectTrigger>
<SelectContent>
+82
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>
);
}
+380
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>;
}
+105
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>
)
}
+144
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>
);
}
+55
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>
);
}
+54
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>
);
}
+99
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>
);
});
+116
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>
</>
);
}
+32
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>
);
}
+52
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>
);
}
+81
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>
);
}
+14
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>
)
}
@@ -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>
);
}
+265
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>
);
}
+24
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}
/>
)
}
+182
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>
);
}
+504
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>
);
}
+155
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);
}
+63
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>
)
}
+44
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>
)
}
+144
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>
);
}
+13 -14
View File
@@ -1,11 +1,10 @@
"use client"
"use client";
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils";
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<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -19,8 +18,8 @@ const TabsList = React.forwardRef<
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -29,13 +28,13 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
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
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -49,7 +48,7 @@ const TabsContent = React.forwardRef<
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };