mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Improve sample select UI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Improve sample select UI
|
||||
@@ -1,13 +1,5 @@
|
||||
import { Fragment, type ReactElement, type ReactNode } from 'react';
|
||||
import {
|
||||
generateSample,
|
||||
type EndpointSample,
|
||||
EndpointSamples,
|
||||
} from '@/utils/generate-sample';
|
||||
import * as CURL from '@/requests/curl';
|
||||
import * as JS from '@/requests/javascript';
|
||||
import * as Go from '@/requests/go';
|
||||
import * as Python from '@/requests/python';
|
||||
import { generateSample, type EndpointSample } from '@/utils/generate-sample';
|
||||
import type {
|
||||
CallbackObject,
|
||||
MethodInformation,
|
||||
@@ -15,20 +7,15 @@ import type {
|
||||
RenderContext,
|
||||
SecurityRequirementObject,
|
||||
} from '@/types';
|
||||
import { getPreferredType, NoReference } from '@/utils/schema';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
import { getPreferredType, type NoReference } from '@/utils/schema';
|
||||
import { getSecurities, getSecurityPrefix } from '@/utils/get-security';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { type ResponseTypeProps } from '@/render/renderer';
|
||||
import { Markdown } from './markdown';
|
||||
import { heading } from './heading';
|
||||
import { Schema } from './schema';
|
||||
import { createMethod } from '@/server/create-method';
|
||||
import { methodKeys } from '@/build-routes';
|
||||
|
||||
interface CustomProperty {
|
||||
'x-codeSamples'?: CodeSample[];
|
||||
}
|
||||
import { APIExample } from '@/render/operation/api-example';
|
||||
|
||||
export interface CodeSample {
|
||||
lang: string;
|
||||
@@ -39,12 +26,6 @@ export interface CodeSample {
|
||||
| false;
|
||||
}
|
||||
|
||||
interface CodeSampleCompiled {
|
||||
lang: string;
|
||||
label: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function Operation({
|
||||
type = 'operation',
|
||||
path,
|
||||
@@ -52,8 +33,6 @@ export function Operation({
|
||||
ctx,
|
||||
hasHead,
|
||||
headingLevel = 2,
|
||||
selectedSampleKey,
|
||||
exclusiveSampleKey,
|
||||
}: {
|
||||
type?: 'webhook' | 'operation';
|
||||
path: string;
|
||||
@@ -62,8 +41,6 @@ export function Operation({
|
||||
|
||||
hasHead?: boolean;
|
||||
headingLevel?: number;
|
||||
selectedSampleKey?: string;
|
||||
exclusiveSampleKey?: string;
|
||||
}): ReactElement {
|
||||
const body = method.requestBody;
|
||||
const security = method.security ?? ctx.schema.document.security;
|
||||
@@ -238,13 +215,7 @@ export function Operation({
|
||||
return (
|
||||
<ctx.renderer.API>
|
||||
{info}
|
||||
<APIExample
|
||||
method={method}
|
||||
endpoint={endpoint}
|
||||
ctx={ctx}
|
||||
selectedSampleKey={selectedSampleKey}
|
||||
exclusiveSampleKey={exclusiveSampleKey}
|
||||
/>
|
||||
<APIExample method={method} endpoint={endpoint} ctx={ctx} />
|
||||
</ctx.renderer.API>
|
||||
);
|
||||
} else {
|
||||
@@ -252,154 +223,6 @@ export function Operation({
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSamples: CodeSample[] = [
|
||||
{
|
||||
label: 'cURL',
|
||||
source: CURL.getSampleRequest,
|
||||
lang: 'bash',
|
||||
},
|
||||
{
|
||||
label: 'JavaScript',
|
||||
source: JS.getSampleRequest,
|
||||
lang: 'js',
|
||||
},
|
||||
{
|
||||
label: 'Go',
|
||||
source: Go.getSampleRequest,
|
||||
lang: 'go',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
source: Python.getSampleRequest,
|
||||
lang: 'python',
|
||||
},
|
||||
];
|
||||
|
||||
async function APIExample({
|
||||
method,
|
||||
endpoint,
|
||||
ctx,
|
||||
selectedSampleKey,
|
||||
exclusiveSampleKey,
|
||||
}: {
|
||||
method: MethodInformation;
|
||||
endpoint: EndpointSample;
|
||||
ctx: RenderContext;
|
||||
selectedSampleKey?: string;
|
||||
exclusiveSampleKey?: string;
|
||||
}) {
|
||||
const renderer = ctx.renderer;
|
||||
const children: ReactNode[] = [];
|
||||
|
||||
// fallback for methods that have no request body, we also want to show examples for
|
||||
const existingSamples: EndpointSamples = endpoint.body?.samples ?? {
|
||||
_default: {},
|
||||
};
|
||||
const samples: Record<
|
||||
string,
|
||||
{ samples: CodeSampleCompiled[]; title: string; description?: string }
|
||||
> = {};
|
||||
for (const exampleKey in existingSamples) {
|
||||
samples[exampleKey] = {
|
||||
title: existingSamples[exampleKey]?.summary ?? exampleKey,
|
||||
description: existingSamples[exampleKey]?.description,
|
||||
samples: dedupe([
|
||||
...defaultSamples,
|
||||
...(ctx.generateCodeSamples
|
||||
? await ctx.generateCodeSamples(endpoint)
|
||||
: []),
|
||||
...((method as CustomProperty)['x-codeSamples'] ?? []),
|
||||
]).flatMap<CodeSampleCompiled>((sample) => {
|
||||
if (sample.source === false) return [];
|
||||
|
||||
const result =
|
||||
typeof sample.source === 'function'
|
||||
? sample.source(endpoint, exampleKey)
|
||||
: sample.source;
|
||||
if (result === undefined) return [];
|
||||
|
||||
return {
|
||||
...sample,
|
||||
source: result,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(samples).length > 0) {
|
||||
const sampleTabs: ReactNode[] = [];
|
||||
const titles = [];
|
||||
if (
|
||||
(samples && Object.keys(samples).length === 1 && samples['_default']) ||
|
||||
(exclusiveSampleKey && samples[exclusiveSampleKey])
|
||||
) {
|
||||
// if exclusiveSampleKey is present, we don't use tabs
|
||||
// if only the fallback or non described openapi legacy example is present, we don't use tabs
|
||||
const sampleKey = exclusiveSampleKey ?? '_default';
|
||||
children.push(
|
||||
<renderer.Requests
|
||||
key={`requests-${sampleKey}`}
|
||||
items={samples[sampleKey].samples.map((s) => s.label)}
|
||||
>
|
||||
{samples[sampleKey].samples.map((s) => (
|
||||
<renderer.Request
|
||||
key={`requests-${sampleKey}-${s.label}`}
|
||||
name={s.label}
|
||||
code={s.source}
|
||||
language={s.lang}
|
||||
/>
|
||||
))}
|
||||
</renderer.Requests>,
|
||||
);
|
||||
} else {
|
||||
for (const sampleKey in samples) {
|
||||
const title = samples[sampleKey].title;
|
||||
titles.push(title);
|
||||
sampleTabs.push(
|
||||
<renderer.Sample key={sampleKey} value={title}>
|
||||
{samples[sampleKey].description && (
|
||||
<Markdown text={samples[sampleKey].description} />
|
||||
)}
|
||||
<renderer.Requests
|
||||
key={`requests-${sampleKey}`}
|
||||
items={samples[sampleKey].samples.map((s) => s.label)}
|
||||
>
|
||||
{samples[sampleKey].samples.map((s) => (
|
||||
<renderer.Request
|
||||
key={`requests-${sampleKey}-${s.label}`}
|
||||
name={s.label}
|
||||
code={s.source}
|
||||
language={s.lang}
|
||||
/>
|
||||
))}
|
||||
</renderer.Requests>
|
||||
</renderer.Sample>,
|
||||
);
|
||||
}
|
||||
children.push(
|
||||
<renderer.Samples
|
||||
items={titles}
|
||||
key="samples"
|
||||
defaultValue={selectedSampleKey}
|
||||
>
|
||||
{sampleTabs}
|
||||
</renderer.Samples>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
children.push(
|
||||
<ResponseTabs
|
||||
key="responses"
|
||||
operation={method}
|
||||
ctx={ctx}
|
||||
endpoint={endpoint}
|
||||
/>,
|
||||
);
|
||||
|
||||
return <renderer.APIExample>{children}</renderer.APIExample>;
|
||||
}
|
||||
|
||||
function WebhookCallback({
|
||||
callback,
|
||||
ctx,
|
||||
@@ -435,22 +258,6 @@ function WebhookCallback({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicated labels
|
||||
*/
|
||||
function dedupe(samples: CodeSample[]): CodeSample[] {
|
||||
const set = new Set<string>();
|
||||
const out: CodeSample[] = [];
|
||||
|
||||
for (let i = samples.length - 1; i >= 0; i--) {
|
||||
if (set.has(samples[i].label)) continue;
|
||||
|
||||
set.add(samples[i].label);
|
||||
out.unshift(samples[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function AuthSection({
|
||||
ctx: {
|
||||
schema: { document },
|
||||
@@ -520,66 +327,3 @@ function AuthSection({
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
async function ResponseTabs({
|
||||
endpoint,
|
||||
operation,
|
||||
ctx: { renderer, generateTypeScriptSchema, schema },
|
||||
}: {
|
||||
endpoint: EndpointSample;
|
||||
operation: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}): Promise<ReactElement | null> {
|
||||
const items: string[] = [];
|
||||
const children: ReactNode[] = [];
|
||||
|
||||
if (!operation.responses) return null;
|
||||
for (const code of Object.keys(operation.responses)) {
|
||||
const types: ResponseTypeProps[] = [];
|
||||
let description = operation.responses[code].description;
|
||||
|
||||
if (!description && code in endpoint.responses)
|
||||
description = endpoint.responses[code].schema.description ?? '';
|
||||
|
||||
if (code in endpoint.responses) {
|
||||
types.push({
|
||||
lang: 'json',
|
||||
label: 'Response',
|
||||
code: JSON.stringify(endpoint.responses[code].sample, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
let ts: string | undefined;
|
||||
if (generateTypeScriptSchema) {
|
||||
ts = await generateTypeScriptSchema(endpoint, code);
|
||||
} else if (generateTypeScriptSchema === undefined) {
|
||||
ts = await getTypescriptSchema(endpoint, code, schema.dereferenceMap);
|
||||
}
|
||||
|
||||
if (ts) {
|
||||
types.push({
|
||||
code: ts,
|
||||
lang: 'ts',
|
||||
label: 'TypeScript',
|
||||
});
|
||||
}
|
||||
|
||||
items.push(code);
|
||||
children.push(
|
||||
<renderer.Response key={code} value={code}>
|
||||
<Markdown text={description} />
|
||||
{types.length > 0 ? (
|
||||
<renderer.ResponseTypes>
|
||||
{types.map((type) => (
|
||||
<renderer.ResponseType key={type.lang} {...type} />
|
||||
))}
|
||||
</renderer.ResponseTypes>
|
||||
) : null}
|
||||
</renderer.Response>,
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return <renderer.Responses items={items}>{children}</renderer.Responses>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import * as CURL from '@/requests/curl';
|
||||
import * as JS from '@/requests/javascript';
|
||||
import * as Go from '@/requests/go';
|
||||
import * as Python from '@/requests/python';
|
||||
import type { MethodInformation, RenderContext } from '@/types';
|
||||
import type { EndpointSample } from '@/utils/generate-sample';
|
||||
import { type ReactNode } from 'react';
|
||||
import { Markdown } from '@/render/markdown';
|
||||
import { type CodeSample } from '@/render/operation';
|
||||
import type { ResponseTypeProps } from '@/render/renderer';
|
||||
import { getTypescriptSchema } from '@/utils/get-typescript-schema';
|
||||
|
||||
const defaultSamples: CodeSample[] = [
|
||||
{
|
||||
label: 'cURL',
|
||||
source: CURL.getSampleRequest,
|
||||
lang: 'bash',
|
||||
},
|
||||
{
|
||||
label: 'JavaScript',
|
||||
source: JS.getSampleRequest,
|
||||
lang: 'js',
|
||||
},
|
||||
{
|
||||
label: 'Go',
|
||||
source: Go.getSampleRequest,
|
||||
lang: 'go',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
source: Python.getSampleRequest,
|
||||
lang: 'python',
|
||||
},
|
||||
];
|
||||
|
||||
interface CustomProperty {
|
||||
'x-codeSamples'?: CodeSample[];
|
||||
'x-selectedCodeSample'?: string;
|
||||
'x-exclusiveCodeSample'?: string;
|
||||
}
|
||||
|
||||
interface CodeSampleCompiled {
|
||||
lang: string;
|
||||
label: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface CollectedSample {
|
||||
samples: CodeSampleCompiled[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export async function APIExample({
|
||||
method,
|
||||
endpoint,
|
||||
ctx,
|
||||
}: {
|
||||
method: MethodInformation & CustomProperty;
|
||||
endpoint: EndpointSample;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
const renderer = ctx.renderer;
|
||||
const samples = new Map<string, CollectedSample>();
|
||||
let children: ReactNode;
|
||||
|
||||
for (const [key, sample] of Object.entries(
|
||||
endpoint.body?.samples ?? {
|
||||
// fallback for methods that have no request body, we also want to show examples for
|
||||
_default: {},
|
||||
},
|
||||
)) {
|
||||
samples.set(key, {
|
||||
title: sample?.summary ?? key,
|
||||
description: sample?.description,
|
||||
samples: dedupe([
|
||||
...defaultSamples,
|
||||
...(ctx.generateCodeSamples
|
||||
? await ctx.generateCodeSamples(endpoint)
|
||||
: []),
|
||||
...(method['x-codeSamples'] ?? []),
|
||||
]).flatMap((sample) => {
|
||||
if (sample.source === false) return [];
|
||||
|
||||
const result =
|
||||
typeof sample.source === 'function'
|
||||
? sample.source(endpoint, key)
|
||||
: sample.source;
|
||||
if (result === undefined) return [];
|
||||
|
||||
return {
|
||||
...sample,
|
||||
source: result,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function renderRequest(sample: CollectedSample) {
|
||||
return (
|
||||
<renderer.Requests items={sample.samples.map((s) => s.label)}>
|
||||
{sample.samples.map((s) => (
|
||||
<renderer.Request
|
||||
key={s.label}
|
||||
name={s.label}
|
||||
code={s.source}
|
||||
language={s.lang}
|
||||
/>
|
||||
))}
|
||||
</renderer.Requests>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(samples.size === 1 && samples.has('_default')) ||
|
||||
(method['x-exclusiveCodeSample'] &&
|
||||
samples.has(method['x-exclusiveCodeSample']))
|
||||
) {
|
||||
// if exclusiveSampleKey is present, we don't use tabs
|
||||
// if only the fallback or non described openapi legacy example is present, we don't use tabs
|
||||
children = renderRequest(
|
||||
samples.get(method['x-exclusiveCodeSample'] ?? '_default')!,
|
||||
);
|
||||
} else if (samples.size > 0) {
|
||||
const entries = Array.from(samples.entries());
|
||||
|
||||
children = (
|
||||
<renderer.Samples
|
||||
items={entries.map(([key, sample]) => ({
|
||||
title: sample.title,
|
||||
description: sample.description ? (
|
||||
<Markdown text={sample.description} />
|
||||
) : null,
|
||||
value: key,
|
||||
}))}
|
||||
defaultValue={method['x-selectedCodeSample']}
|
||||
>
|
||||
{entries.map(([key, sample]) => (
|
||||
<renderer.Sample key={key} value={key}>
|
||||
{renderRequest(sample)}
|
||||
</renderer.Sample>
|
||||
))}
|
||||
</renderer.Samples>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<renderer.APIExample>
|
||||
{children}
|
||||
<ResponseTabs operation={method} ctx={ctx} endpoint={endpoint} />
|
||||
</renderer.APIExample>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove duplicated labels
|
||||
*/
|
||||
function dedupe(samples: CodeSample[]): CodeSample[] {
|
||||
const set = new Set<string>();
|
||||
const out: CodeSample[] = [];
|
||||
|
||||
for (let i = samples.length - 1; i >= 0; i--) {
|
||||
if (set.has(samples[i].label)) continue;
|
||||
|
||||
set.add(samples[i].label);
|
||||
out.unshift(samples[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ResponseTabs({
|
||||
endpoint,
|
||||
operation,
|
||||
ctx: { renderer, generateTypeScriptSchema, schema },
|
||||
}: {
|
||||
endpoint: EndpointSample;
|
||||
operation: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}) {
|
||||
if (!operation.responses) return null;
|
||||
|
||||
async function renderResponse(code: string) {
|
||||
const types: ResponseTypeProps[] = [];
|
||||
|
||||
let description = operation.responses?.[code].description;
|
||||
if (!description && code in endpoint.responses)
|
||||
description = endpoint.responses[code].schema.description ?? '';
|
||||
|
||||
if (code in endpoint.responses) {
|
||||
types.push({
|
||||
lang: 'json',
|
||||
label: 'Response',
|
||||
code: JSON.stringify(endpoint.responses[code].sample, null, 2),
|
||||
});
|
||||
}
|
||||
|
||||
let ts: string | undefined;
|
||||
if (generateTypeScriptSchema) {
|
||||
ts = await generateTypeScriptSchema(endpoint, code);
|
||||
} else if (generateTypeScriptSchema === undefined) {
|
||||
ts = await getTypescriptSchema(endpoint, code, schema.dereferenceMap);
|
||||
}
|
||||
|
||||
if (ts) {
|
||||
types.push({
|
||||
code: ts,
|
||||
lang: 'ts',
|
||||
label: 'TypeScript',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<renderer.Response value={code}>
|
||||
{description ? <Markdown text={description} /> : null}
|
||||
{types.length > 0 ? (
|
||||
<renderer.ResponseTypes>
|
||||
{types.map((type) => (
|
||||
<renderer.ResponseType key={type.lang} {...type} />
|
||||
))}
|
||||
</renderer.ResponseTypes>
|
||||
) : null}
|
||||
</renderer.Response>
|
||||
);
|
||||
}
|
||||
|
||||
const codes = Object.keys(operation.responses);
|
||||
if (codes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<renderer.Responses items={codes}>
|
||||
{codes.map((code) => (
|
||||
<renderer.Response key={code} value={code}>
|
||||
{renderResponse(code)}
|
||||
</renderer.Response>
|
||||
))}
|
||||
</renderer.Responses>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/ui';
|
||||
import type { MethodInformation, RenderContext, ServerObject } from '@/types';
|
||||
import { Playground } from '@/render/playground';
|
||||
import { Sample, Samples } from '@/ui/client';
|
||||
|
||||
export interface ResponsesProps {
|
||||
items: string[];
|
||||
@@ -54,8 +55,13 @@ export interface SampleProps {
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export interface SamplesProps {
|
||||
items: string[];
|
||||
items: {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
children: ReactNode;
|
||||
defaultValue?: string;
|
||||
}
|
||||
@@ -68,6 +74,8 @@ export interface ResponseTypeProps {
|
||||
|
||||
export interface RootProps {
|
||||
baseUrl?: string;
|
||||
shikiOptions?: RenderContext['shikiOptions'];
|
||||
|
||||
servers: ServerObject[];
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -144,8 +152,8 @@ export function createRenders(
|
||||
Requests: (props) => (
|
||||
<Tabs groupId="fumadocs_openapi_requests" {...props} />
|
||||
),
|
||||
Samples: (props) => <Tabs {...props} />,
|
||||
Sample: Tab,
|
||||
Samples,
|
||||
Sample,
|
||||
|
||||
Request: (props) => (
|
||||
<Tab value={props.name}>
|
||||
|
||||
@@ -47,14 +47,6 @@ export interface WebhookItem {
|
||||
export interface OperationItem {
|
||||
path: string;
|
||||
method: OpenAPIV3_1.HttpMethods;
|
||||
/**
|
||||
* The key of the sample to be selected
|
||||
*/
|
||||
selectedSampleKey?: string;
|
||||
/**
|
||||
* Only this sample will be shown
|
||||
*/
|
||||
exclusiveSampleKey?: string;
|
||||
}
|
||||
|
||||
export async function APIPage(props: ApiPageProps) {
|
||||
@@ -87,8 +79,6 @@ export async function APIPage(props: ApiPageProps) {
|
||||
path={item.path}
|
||||
ctx={ctx}
|
||||
hasHead={hasHead}
|
||||
selectedSampleKey={item.selectedSampleKey}
|
||||
exclusiveSampleKey={item.exclusiveSampleKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { BuildPageTreeOptions } from 'fumadocs-core/source';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { getBadgeColor } from '@/ui/components/variants';
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
|
||||
/**
|
||||
* Source API Integration
|
||||
@@ -21,31 +20,15 @@ export const attachFile: BuildPageTreeOptions['attachFile'] = (node, file) => {
|
||||
}
|
||||
|
||||
if (method) {
|
||||
const color = getBadgeColor(method);
|
||||
|
||||
node.name = (
|
||||
<>
|
||||
{node.name}{' '}
|
||||
<span
|
||||
className={badgeVariants({ className: 'ms-auto text-nowrap', color })}
|
||||
>
|
||||
<MethodLabel className="ms-auto text-xs text-nowrap">
|
||||
{method}
|
||||
</span>
|
||||
</MethodLabel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const badgeVariants = cva('rounded-full border px-1.5 text-xs font-medium', {
|
||||
variants: {
|
||||
color: {
|
||||
green: 'bg-green-400/20 text-green-600 dark:text-green-400',
|
||||
yellow: 'bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
|
||||
red: 'bg-red-400/20 text-red-600 dark:text-red-400',
|
||||
blue: 'bg-blue-400/20 text-blue-600 dark:text-blue-400',
|
||||
orange: 'bg-orange-400/20 text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,42 +1,16 @@
|
||||
'use client';
|
||||
import { type HTMLAttributes, type ReactNode } from 'react';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ApiProvider } from '@/ui/contexts/api';
|
||||
import { type RootProps } from '@/render/renderer';
|
||||
import type { RenderContext } from '@/types';
|
||||
|
||||
export const APIPlayground = dynamic(() =>
|
||||
import('./playground').then((mod) => mod.APIPlayground),
|
||||
);
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
className,
|
||||
shikiOptions,
|
||||
servers,
|
||||
...props
|
||||
}: RootProps & {
|
||||
shikiOptions: RenderContext['shikiOptions'];
|
||||
} & HTMLAttributes<HTMLDivElement>): ReactNode {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-24 text-sm text-fd-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider
|
||||
servers={servers}
|
||||
shikiOptions={shikiOptions}
|
||||
defaultBaseUrl={baseUrl}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export const Samples = dynamic(() =>
|
||||
import('./sample-select').then((mod) => mod.Samples),
|
||||
);
|
||||
|
||||
export const Sample = dynamic(() =>
|
||||
import('./sample-select').then((mod) => mod.Sample),
|
||||
);
|
||||
|
||||
export { useSchemaContext } from './contexts/schema';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
|
||||
const variants = cva('font-mono font-medium', {
|
||||
export const badgeVariants = cva('font-mono font-medium', {
|
||||
variants: {
|
||||
color: {
|
||||
green: 'text-green-600 dark:text-green-400',
|
||||
@@ -14,7 +14,9 @@ const variants = cva('font-mono font-medium', {
|
||||
},
|
||||
});
|
||||
|
||||
function getBadgeColor(method: string): VariantProps<typeof variants>['color'] {
|
||||
export function getBadgeColor(
|
||||
method: string,
|
||||
): VariantProps<typeof badgeVariants>['color'] {
|
||||
switch (method.toUpperCase()) {
|
||||
case 'PUT':
|
||||
return 'yellow';
|
||||
@@ -29,6 +31,27 @@ function getBadgeColor(method: string): VariantProps<typeof variants>['color'] {
|
||||
}
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: Omit<HTMLAttributes<HTMLSpanElement>, 'color'> &
|
||||
VariantProps<typeof badgeVariants>) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
badgeVariants({
|
||||
color,
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MethodLabel({
|
||||
children,
|
||||
...props
|
||||
@@ -36,16 +59,8 @@ export function MethodLabel({
|
||||
children: string;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
className={cn(
|
||||
variants({
|
||||
color: getBadgeColor(children),
|
||||
}),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<Badge {...props} color={getBadgeColor(children)}>
|
||||
{children.toUpperCase()}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const SelectTrigger = forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-10 items-center w-full rounded-md border px-3 py-2 text-start text-[13px] text-fd-foreground hover:bg-fd-accent focus:outline-none focus:ring-2 focus:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex min-h-10 items-center w-full rounded-md border px-3 py-2 text-start text-[13px] text-fd-foreground hover:bg-fd-accent focus:outline-none focus:ring-2 focus:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'rounded-xl border px-1.5 py-1 text-xs font-medium leading-[12px]',
|
||||
{
|
||||
variants: {
|
||||
color: {
|
||||
green: 'bg-green-400/20 text-green-600 dark:text-green-400',
|
||||
yellow: 'bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
|
||||
red: 'bg-red-400/20 text-red-600 dark:text-red-400',
|
||||
blue: 'bg-blue-400/20 text-blue-600 dark:text-blue-400',
|
||||
orange: 'bg-orange-400/20 text-orange-600 dark:text-orange-400',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function getBadgeColor(
|
||||
method: string,
|
||||
): VariantProps<typeof badgeVariants>['color'] {
|
||||
switch (method) {
|
||||
case 'PUT':
|
||||
return 'yellow';
|
||||
case 'PATCH':
|
||||
return 'orange';
|
||||
case 'POST':
|
||||
return 'blue';
|
||||
case 'DELETE':
|
||||
return 'red';
|
||||
default:
|
||||
return 'green';
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
import { buttonVariants, cn } from 'fumadocs-ui/components/api';
|
||||
import { type HTMLAttributes, type ReactNode } from 'react';
|
||||
import { badgeVariants } from '@/ui/components/variants';
|
||||
import type { PropertyProps } from '@/render/renderer';
|
||||
import { Badge } from '@/ui/components/method-label';
|
||||
import type { PropertyProps, RootProps } from '@/render/renderer';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from 'fumadocs-ui/components/ui/collapsible';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ApiProvider } from '@/ui/contexts/api';
|
||||
|
||||
export { Root, useSchemaContext, APIPlayground } from './client';
|
||||
export { useSchemaContext, APIPlayground } from './client';
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
className,
|
||||
shikiOptions,
|
||||
servers,
|
||||
...props
|
||||
}: RootProps & HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-24 text-sm text-fd-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ApiProvider
|
||||
servers={servers}
|
||||
shikiOptions={shikiOptions}
|
||||
defaultBaseUrl={baseUrl}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIInfo({
|
||||
className,
|
||||
@@ -55,12 +83,14 @@ export function Property({
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<code>{name}</code>
|
||||
{required ? (
|
||||
<div className={cn(badgeVariants({ color: 'red' }))}>Required</div>
|
||||
<Badge color="red" className="text-xs">
|
||||
Required
|
||||
</Badge>
|
||||
) : null}
|
||||
{deprecated ? (
|
||||
<div className={cn(badgeVariants({ color: 'yellow' }))}>
|
||||
<Badge color="yellow" className="text-xs">
|
||||
Deprecated
|
||||
</div>
|
||||
</Badge>
|
||||
) : null}
|
||||
<span className="ms-auto text-xs font-mono text-fd-muted-foreground">
|
||||
{type}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createContext, use, useState } from 'react';
|
||||
import type { SampleProps, SamplesProps } from '@/render/renderer';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
|
||||
const ActiveSampleContext = createContext('');
|
||||
|
||||
export function Samples({
|
||||
items,
|
||||
defaultValue = items[0].value,
|
||||
children,
|
||||
}: SamplesProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const active = value === '' ? defaultValue : value;
|
||||
const defaultItem = items.find((item) => item.value === defaultValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select value={value} onValueChange={setValue}>
|
||||
<SelectTrigger className="not-prose">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
defaultItem ? <SelectDisplay {...defaultItem} /> : null
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectDisplay {...item} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<ActiveSampleContext value={active}>{children}</ActiveSampleContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectDisplay(item: SamplesProps['items'][number]) {
|
||||
return (
|
||||
<>
|
||||
<span className="font-medium text-sm">{item.title}</span>
|
||||
<span className="text-fd-muted-foreground">{item.description}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sample({ value, children }: SampleProps) {
|
||||
const active = use(ActiveSampleContext);
|
||||
if (value !== active) return;
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
".source/**",
|
||||
"!.contentlayer/.cache/**",
|
||||
".next/**",
|
||||
"content/openapi/**",
|
||||
"!.next/cache/**",
|
||||
"dist/**",
|
||||
"out"
|
||||
|
||||
Reference in New Issue
Block a user