docs: improve api endpoint component (#8712)

This commit is contained in:
Taesu
2026-03-21 01:32:38 +09:00
committed by GitHub
parent 0410a40a22
commit 7b658fe656
3 changed files with 186 additions and 137 deletions

View File

@@ -22,7 +22,7 @@ function ApiMethodTabs({
return ( return (
<provider.Provider value={{ current, setCurrent }}> <provider.Provider value={{ current, setCurrent }}>
<div <div
data-slot="tabs" data-slot="api-method-tabs"
className={cn("flex flex-col gap-2", className)} className={cn("flex flex-col gap-2", className)}
{...props} {...props}
/> />

View File

@@ -183,14 +183,9 @@ export const APIMethod = ({
); );
const serverTabContent = ( const serverTabContent = (
<> <div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
{isClientOnly || isServerOnly ? null : ( {isClientOnly || isServerOnly ? null : (
<Endpoint <Endpoint method={method || "GET"} path={path} className="" />
method={method || "GET"}
path={path}
isServerOnly={isServerOnly ?? false}
className=""
/>
)} )}
{serverOnlyNote || note ? ( {serverOnlyNote || note ? (
<Note> <Note>
@@ -212,7 +207,7 @@ export const APIMethod = ({
) : null} ) : null}
</div> </div>
{!isClientOnly ? <TypeTable props={props} isServer /> : null} {!isClientOnly ? <TypeTable props={props} isServer /> : null}
</> </div>
); );
if (isExternalOnly) { if (isExternalOnly) {
@@ -234,48 +229,53 @@ export const APIMethod = ({
defaultValue={isServerOnly ? "server" : "client"} defaultValue={isServerOnly ? "server" : "client"}
className="gap-0 w-full" className="gap-0 w-full"
> >
<ApiMethodTabsList className="relative flex justify-start w-full p-0 bg-background dark:bg-[#050505] hover:[&>div>a>button]:opacity-100"> <ApiMethodTabsList className="relative flex justify-start w-full p-0 bg-background hover:[&>div>a>button]:opacity-100">
<ApiMethodTabsTrigger {(
value="client" [
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-fd-secondary hover:bg-fd-secondary/70 bg-fd-secondary/50 border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none dark:bg-[#050505] dark:hover:bg-[#0a0a0a] dark:data-[state=active]:bg-[#0a0a0a] dark:border-white/[0.06] dark:hover:border-white/10 dark:data-[state=active]:border-white/10" {
> value: "client",
<svg label: "Client",
xmlns="http://www.w3.org/2000/svg" icon: (
width="1em" <>
height="1em" <rect width="20" height="14" x="2" y="3" rx="2" />
viewBox="0 0 24 24" <path d="M8 21h8m-4-4v4" />
fill="none" </>
stroke="currentColor" ),
strokeWidth="2" },
strokeLinecap="round" {
strokeLinejoin="round" value: "server",
label: "Server",
icon: (
<>
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
<path d="M6 6h.01M6 18h.01" />
</>
),
},
] as const
).map((tab) => (
<ApiMethodTabsTrigger
key={tab.value}
value={tab.value}
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-fd-muted/80 hover:bg-fd-secondary/70 bg-background border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none dark:bg-[#050505] dark:hover:bg-[#0a0a0a] dark:data-[state=active]:bg-fd-muted/80 dark:border-white/[0.06] dark:hover:border-white/10 dark:data-[state=active]:border-white/10"
> >
<rect width="20" height="14" x="2" y="3" rx="2" /> <svg
<path d="M8 21h8m-4-4v4" /> xmlns="http://www.w3.org/2000/svg"
</svg> width="1em"
<span>Client</span> height="1em"
</ApiMethodTabsTrigger> viewBox="0 0 24 24"
<ApiMethodTabsTrigger fill="none"
value="server" stroke="currentColor"
className="transition-all duration-150 ease-in-out max-w-[100px] data-[state=active]:bg-fd-secondary hover:bg-fd-secondary/70 bg-fd-secondary/50 border hover:border-primary/15 cursor-pointer data-[state=active]:border-primary/10 rounded-none dark:bg-[#050505] dark:hover:bg-[#0a0a0a] dark:data-[state=active]:bg-[#0a0a0a] dark:border-white/[0.06] dark:hover:border-white/10 dark:data-[state=active]:border-white/10" strokeWidth="2"
> strokeLinecap="round"
<svg strokeLinejoin="round"
xmlns="http://www.w3.org/2000/svg" >
width="1em" {tab.icon}
height="1em" </svg>
viewBox="0 0 24 24" <span>{tab.label}</span>
fill="none" </ApiMethodTabsTrigger>
stroke="currentColor" ))}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
<path d="M6 6h.01M6 18h.01" />
</svg>
<span>Server</span>
</ApiMethodTabsTrigger>
<div className="absolute right-0"> <div className="absolute right-0">
<a href={`#api-method${pathId}`}> <a href={`#api-method${pathId}`}>
<Button <Button
@@ -289,43 +289,43 @@ export const APIMethod = ({
</div> </div>
</ApiMethodTabsList> </ApiMethodTabsList>
<ApiMethodTabsContent value="client"> <ApiMethodTabsContent value="client">
{isServerOnly ? null : ( <div className="border shadow-sm [&_figure]:my-0 [&_figure]:border-0 [&_figure]:shadow-none [&_figure]:rounded-none [&_.fd-scroll-container]:bg-transparent">
<Endpoint {isServerOnly ? null : (
method={method || "GET"} <Endpoint method={method || "GET"} path={path} />
path={path} )}
isServerOnly={isServerOnly ?? false} {clientOnlyNote || note ? (
/> <Note>
)} {note && tsxifyBackticks(note)}
{clientOnlyNote || note ? ( {clientOnlyNote ? (
<Note> <>
{note && tsxifyBackticks(note)} {note ? <br /> : null}
{clientOnlyNote ? ( {tsxifyBackticks(clientOnlyNote)}
<> </>
{note ? <br /> : null} ) : null}
{tsxifyBackticks(clientOnlyNote)} </Note>
</> ) : null}
<div className={cn("relative w-full")}>
<DynamicCodeBlock
code={`${code_prefix}${
noResult
? ""
: `const { data${
resultVariable === "data" ? "" : `: ${resultVariable}`
}, error } = `
}await authClient.${authClientMethodPath}(${clientBody});${code_suffix}`}
lang="ts"
allowCopy={!isServerOnly}
/>
{isServerOnly ? (
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
<span>This is a server-only endpoint</span>
</div>
) : null} ) : null}
</Note> </div>
) : null} {!isServerOnly ? (
<div className={cn("relative w-full")}> <TypeTable props={props} isServer={false} />
<DynamicCodeBlock
code={`${code_prefix}${
noResult
? ""
: `const { data${
resultVariable === "data" ? "" : `: ${resultVariable}`
}, error } = `
}await authClient.${authClientMethodPath}(${clientBody});${code_suffix}`}
lang="ts"
allowCopy={!isServerOnly}
/>
{isServerOnly ? (
<div className="flex absolute inset-0 justify-center items-center w-full h-full rounded-lg border backdrop-brightness-50 backdrop-blur-xs border-border">
<span>This is a server-only endpoint</span>
</div>
) : null} ) : null}
</div> </div>
{!isServerOnly ? <TypeTable props={props} isServer={false} /> : null}
</ApiMethodTabsContent> </ApiMethodTabsContent>
<ApiMethodTabsContent value="server"> <ApiMethodTabsContent value="server">
{serverTabContent} {serverTabContent}
@@ -389,8 +389,8 @@ function TypeTable({
if (!filteredProps.length) return null; if (!filteredProps.length) return null;
return ( return (
<div className="border border-border dark:border-white/[0.06] bg-fd-secondary/30 dark:bg-[#050505] mt-0"> <div className="mt-0">
<div className="flex items-center gap-2 px-3.5 py-2 border-b border-border dark:border-white/[0.06]"> <div className="flex items-center gap-2 px-3.5 py-2 border-y border-border bg-fd-muted/80">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="12" width="12"
@@ -408,46 +408,93 @@ function TypeTable({
<path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" /> <path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" />
<path d="m15 9 6-6" /> <path d="m15 9 6-6" />
</svg> </svg>
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider"> <span className="text-xs font-medium text-muted-foreground tracking-wider">
Parameters Parameters
</span> </span>
</div> </div>
<div className="divide-y divide-border dark:divide-white/[0.04]"> <PropertyList props={filteredProps} />
{filteredProps.map((prop) => ( </div>
<div );
key={`${prop.path.join(".")}.${prop.propName}`} }
className="group px-3.5 py-2.5 hover:bg-fd-secondary/50 dark:hover:bg-white/[0.02] transition-colors"
style={{ function PropertyItem({ prop }: { prop: Property }) {
paddingLeft: `${0.875 + prop.path.length * 1}rem`, return (
}} <div className="flex items-center gap-2 flex-wrap">
> <code className="text-xs font-semibold text-foreground/90">
<div className="flex items-center gap-2 flex-wrap"> {prop.propName}
<code className="text-[13px] font-semibold text-foreground/90"> </code>
{prop.propName} <span className="text-xs font-mono text-foreground/60 font-medium">
</code> {prop.type}
<span className="text-[11px] font-mono text-muted-foreground/60"> {prop.isNullable ? " | null" : ""}
{prop.type} </span>
{prop.isNullable ? " | null" : ""} {!prop.isOptional && (
</span> <span className="text-[10px] font-mono font-medium text-amber-600 dark:text-amber-500/80">
{!prop.isOptional && ( required
<span className="text-[9px] font-medium uppercase tracking-widest text-amber-600 dark:text-amber-500/80"> </span>
required )}
</span> {prop.isServerOnly && (
)} <span className="text-[10px] font-mono font-medium text-blue-600 dark:text-blue-400/80">
{prop.isServerOnly && ( server
<span className="text-[9px] font-medium uppercase tracking-widest text-blue-600 dark:text-blue-400/80"> </span>
server )}
</span> </div>
)} );
}
function PropertyList({
props,
nested = false,
}: {
props: Property[];
nested?: boolean;
}) {
const groups: { prop: Property; children: Property[] }[] = [];
let i = 0;
while (i < props.length) {
const prop = props[i];
if (prop.type === "Object") {
const parentSegments = [...prop.path, prop.propName];
const children: Property[] = [];
i++;
while (
i < props.length &&
props[i].path.length >= parentSegments.length &&
parentSegments.every((seg, idx) => props[i].path[idx] === seg)
) {
children.push(props[i]);
i++;
}
groups.push({ prop, children });
} else {
groups.push({ prop, children: [] });
i++;
}
}
return (
<div className="divide-y divide-border">
{groups.map((group) => (
<div
key={`${group.prop.path.join(".")}.${group.prop.propName}`}
className={cn(
nested ? "px-3 py-3" : "px-3.5 py-3",
group.children.length > 0 && "pb-3",
)}
>
<PropertyItem prop={group.prop} />
{group.prop.description && (
<p className="mt-1 mb-0 text-sm leading-relaxed max-w-xl">
{tsxifyBackticks(group.prop.description)}
</p>
)}
{group.children.length > 0 && (
<div className="mt-3 border rounded-md overflow-hidden">
<PropertyList props={group.children} nested />
</div> </div>
{prop.description && ( )}
<p className="mt-1 text-[12px] text-muted-foreground/70 leading-relaxed max-w-xl"> </div>
{tsxifyBackticks(prop.description)} ))}
</p>
)}
</div>
))}
</div>
</div> </div>
); );
} }
@@ -885,11 +932,13 @@ function createServerBody({
function Note({ children }: { children: ReactNode }) { function Note({ children }: { children: ReactNode }) {
return ( return (
<div className="flex relative flex-col gap-2 p-3 mb-2 w-full break-words rounded-md border text-md text-wrap border-border bg-fd-secondary/50"> <div className="flex relative flex-col gap-2 p-3 mb-2 w-full wrap-break-word rounded-md border-b text-md text-wrap bg-fd-muted/80">
<span className="-mb-1 w-full text-xs select-none text-muted-foreground"> <span className="-mb-1 w-full text-xs select-none text-foreground/80 font-medium">
Notes Notes
</span> </span>
<p className="mt-0 mb-0 text-sm">{children as any}</p> <p className="mt-0 mb-0 text-sm text-fd-muted-foreground">
{children as any}
</p>
</div> </div>
); );
} }

View File

@@ -1,18 +1,20 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const methodColors: Record<string, string> = { type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
GET: "text-emerald-700 dark:text-emerald-400",
POST: "text-blue-700 dark:text-blue-400", const methodColors: Record<HttpMethod, string> = {
PUT: "text-amber-700 dark:text-amber-400", GET: "text-green-600 dark:text-green-500",
DELETE: "text-red-700 dark:text-red-400", POST: "text-yellow-600 dark:text-yellow-600",
PUT: "text-blue-600 dark:text-blue-400",
DELETE: "text-red-600 dark:text-red-400",
}; };
function Method({ method }: { method: "POST" | "GET" | "DELETE" | "PUT" }) { function Method({ method }: { method: HttpMethod }) {
return ( return (
<span <span
className={cn( className={cn(
"text-[11px] font-bold font-mono uppercase tracking-wider", "text-xs font-bold font-mono uppercase",
methodColors[method], methodColors[method],
)} )}
> >
@@ -24,23 +26,21 @@ function Method({ method }: { method: "POST" | "GET" | "DELETE" | "PUT" }) {
export function Endpoint({ export function Endpoint({
path, path,
method, method,
isServerOnly,
className, className,
}: { }: {
path: string; path: string;
method: "POST" | "GET" | "DELETE" | "PUT"; method: HttpMethod;
isServerOnly?: boolean;
className?: string; className?: string;
}) { }) {
return ( return (
<div <div
className={cn( className={cn(
"relative flex items-center w-full gap-3 px-3.5 py-2.5 border-t border-x border-border bg-fd-secondary/50 dark:border-white/[0.06] dark:bg-[#050505] group", "relative flex items-center w-full gap-2 px-3.5 py-1 border-b border-border bg-fd-muted/80 group",
className, className,
)} )}
> >
<Method method={method} /> <Method method={method} />
<span className="font-mono text-[12px] text-muted-foreground/70"> <span className="font-mono text-[13px] text-foreground/80 font-medium">
{path} {path}
</span> </span>
</div> </div>