mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support useScalar option
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Support `useScalar` option
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
background-color: var(--color-fd-background);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.7",
|
||||
"@scalar/api-client-react": "^1.1.17",
|
||||
"@shikijs/rehype": "^2.1.0",
|
||||
"@theguild/remark-mermaid": "^0.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function generateDocs() {
|
||||
input: ['./museum.yaml'],
|
||||
output: './content/docs/openapi',
|
||||
per: 'operation',
|
||||
groupBy: 'tag',
|
||||
}),
|
||||
Typescript.generateFiles({
|
||||
input: ['./content/docs/**/*.model.mdx'],
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"openapi-sampler": "^1.6.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"remark": "^15.0.1",
|
||||
@@ -58,6 +59,7 @@
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scalar/api-client-react": "^1.1.17",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "22.10.9",
|
||||
"@types/openapi-sampler": "^1.0.3",
|
||||
@@ -71,10 +73,16 @@
|
||||
"tsconfig": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scalar/api-client-react": "*",
|
||||
"next": "14.x.x || 15.x.x",
|
||||
"react": "18.x.x || 19.x.x",
|
||||
"react-dom": "18.x.x || 19.x.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@scalar/api-client-react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ParsedSchema,
|
||||
} from '@/utils/schema';
|
||||
import { getSecurities } from '@/utils/get-security';
|
||||
import { ScalarPlayground } from '@/ui';
|
||||
|
||||
interface BaseRequestField {
|
||||
name: string;
|
||||
@@ -103,6 +104,10 @@ export function Playground({
|
||||
method: MethodInformation;
|
||||
ctx: RenderContext;
|
||||
}): ReactNode {
|
||||
if (ctx.useScalar) {
|
||||
return <ScalarPlayground method={method.method} path={path} />;
|
||||
}
|
||||
|
||||
let currentId = 0;
|
||||
const bodyContent = method.requestBody?.content;
|
||||
const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Property,
|
||||
ObjectCollapsible,
|
||||
APIPlayground,
|
||||
APIInfoHeader,
|
||||
} from '@/ui';
|
||||
import type { RenderContext, ServerObject } from '@/types';
|
||||
|
||||
@@ -93,6 +94,7 @@ export type {
|
||||
|
||||
export function createRenders(
|
||||
shikiOptions: RenderContext['shikiOptions'],
|
||||
useScalar: boolean,
|
||||
): Renderer {
|
||||
return {
|
||||
Root: (props) => (
|
||||
@@ -101,7 +103,13 @@ export function createRenders(
|
||||
</Root>
|
||||
),
|
||||
API,
|
||||
APIInfo,
|
||||
APIInfo: ({ children, head, ...props }) => (
|
||||
<APIInfo>
|
||||
{head}
|
||||
{!useScalar ? <APIInfoHeader {...props} /> : null}
|
||||
{children}
|
||||
</APIInfo>
|
||||
),
|
||||
APIExample,
|
||||
Responses: Tabs,
|
||||
Response: Tab,
|
||||
|
||||
@@ -189,7 +189,12 @@ export function Schema({
|
||||
return (
|
||||
<renderer.ObjectCollapsible
|
||||
key={`mentioned:${idx.toString()}`}
|
||||
name={s.title ?? `Object ${(idx + 1).toString()}`}
|
||||
name={
|
||||
s.title ??
|
||||
(mentionedObjectTypes.length === 1
|
||||
? 'Show Attributes'
|
||||
: `Object ${idx + 1}`)
|
||||
}
|
||||
>
|
||||
<Schema
|
||||
name="element"
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import { type DocumentInput, processDocument } from '@/utils/process-document';
|
||||
import { getUrl } from '@/utils/server-url';
|
||||
import { ScalarProvider } from '@/ui';
|
||||
|
||||
type ApiPageContextProps = Pick<
|
||||
Partial<RenderContext>,
|
||||
@@ -16,6 +17,7 @@ type ApiPageContextProps = Pick<
|
||||
| 'generateCodeSamples'
|
||||
| 'proxyUrl'
|
||||
| 'showResponseSchema'
|
||||
| 'useScalar'
|
||||
>;
|
||||
|
||||
export interface ApiPageProps extends ApiPageContextProps {
|
||||
@@ -63,7 +65,8 @@ export async function APIPage(props: ApiPageProps) {
|
||||
const processed = await processDocument(props.document, disableCache);
|
||||
const ctx = await getContext(processed, props);
|
||||
const { document } = processed;
|
||||
return (
|
||||
|
||||
const children = (
|
||||
<ctx.renderer.Root baseUrl={ctx.baseUrl} servers={ctx.servers}>
|
||||
{operations?.map((item) => {
|
||||
const pathItem = document.paths?.[item.path];
|
||||
@@ -109,6 +112,12 @@ export async function APIPage(props: ApiPageProps) {
|
||||
})}
|
||||
</ctx.renderer.Root>
|
||||
);
|
||||
|
||||
return ctx.useScalar ? (
|
||||
<ScalarProvider spec={processed.downloaded}>{children}</ScalarProvider>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
export async function getContext(
|
||||
@@ -124,12 +133,13 @@ export async function getContext(
|
||||
const server = servers[0];
|
||||
|
||||
return {
|
||||
useScalar: options.useScalar ?? false,
|
||||
document: document,
|
||||
dereferenceMap,
|
||||
proxyUrl: options.proxyUrl,
|
||||
showResponseSchema: options.showResponseSchema,
|
||||
renderer: {
|
||||
...createRenders(options.shikiOptions),
|
||||
...createRenders(options.shikiOptions, options.useScalar ?? false),
|
||||
...options.renderer,
|
||||
},
|
||||
shikiOptions: options.shikiOptions,
|
||||
|
||||
@@ -33,6 +33,11 @@ type Awaitable<T> = T | Promise<T>;
|
||||
export type DereferenceMap = Map<unknown, string>;
|
||||
|
||||
export interface RenderContext {
|
||||
/**
|
||||
* Use Scalar for API Playground
|
||||
*/
|
||||
useScalar: boolean;
|
||||
|
||||
/**
|
||||
* The url of proxy to avoid CORS issues
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,9 @@ export const APIPlayground = dynamic(() =>
|
||||
|
||||
export const ServerSelect = dynamic(() => import('./server-select'));
|
||||
|
||||
export const ScalarPlayground = dynamic(() => import('./scalar/index'));
|
||||
export const ScalarProvider = dynamic(() => import('./scalar/provider'));
|
||||
|
||||
export function Root({
|
||||
children,
|
||||
baseUrl,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from 'fumadocs-ui/components/ui/collapsible';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
export function CollapsiblePanel({
|
||||
title,
|
||||
children,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Collapsible {...props}>
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
buttonVariants({ color: 'outline', size: 'sm' }),
|
||||
'group rounded-full px-2 py-1.5 text-fd-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
<ChevronDown className="size-4 group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="-mx-2">
|
||||
<div className="flex flex-col gap-4 p-2 pb-0">{children}</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ const FormItem = forwardRef<
|
||||
FormItem.displayName = 'FormItem';
|
||||
|
||||
export const labelVariants = cva(
|
||||
'font-mono text-xs text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
'font-medium text-[13px] text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
);
|
||||
|
||||
const FormLabel = forwardRef<
|
||||
@@ -93,7 +93,7 @@ const FormLabel = forwardRef<
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), isError && 'text-red-500', className)}
|
||||
className={cn(isError && 'text-red-500', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
import { Fragment, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { badgeVariants, getBadgeColor } from '@/ui/components/variants';
|
||||
import type { APIInfoProps, PropertyProps } from '@/render/renderer';
|
||||
import { CopyRouteButton, ServerSelect } from '@/ui/client';
|
||||
import { CollapsiblePanel } from '@/ui/components/collapsible';
|
||||
|
||||
export { Root, useSchemaContext, APIPlayground } from './client';
|
||||
export {
|
||||
Root,
|
||||
useSchemaContext,
|
||||
APIPlayground,
|
||||
ScalarPlayground,
|
||||
ScalarProvider,
|
||||
} from './client';
|
||||
|
||||
function Route({ route }: { route: string }): ReactNode {
|
||||
const segments = route.split('/').filter((part) => part.length > 0);
|
||||
@@ -28,34 +34,29 @@ function Route({ route }: { route: string }): ReactNode {
|
||||
|
||||
export function APIInfo({
|
||||
className,
|
||||
route,
|
||||
badgeClassname,
|
||||
method = 'GET',
|
||||
head,
|
||||
...props
|
||||
}: APIInfoProps &
|
||||
HTMLAttributes<HTMLDivElement> & {
|
||||
badgeClassname?: string;
|
||||
}) {
|
||||
}: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div className={cn('min-w-0 flex-1', className)} {...props}>
|
||||
{head}
|
||||
<div className="not-prose mb-4 rounded-lg border bg-fd-card p-3 text-fd-card-foreground shadow-lg">
|
||||
<div className="-mx-1 flex flex-row items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
badgeVariants({ color: getBadgeColor(method) }),
|
||||
badgeClassname,
|
||||
)}
|
||||
>
|
||||
{method}
|
||||
</span>
|
||||
<Route route={route} />
|
||||
<CopyRouteButton className="ms-auto size-6 p-1.5" route={route} />
|
||||
</div>
|
||||
<ServerSelect />
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function APIInfoHeader({
|
||||
method,
|
||||
route,
|
||||
}: Omit<APIInfoProps, 'head' | 'children'>) {
|
||||
return (
|
||||
<div className="not-prose mb-4 rounded-lg border bg-fd-card p-3 text-fd-card-foreground shadow-lg">
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<span className={cn(badgeVariants({ color: getBadgeColor(method) }))}>
|
||||
{method}
|
||||
</span>
|
||||
<Route route={route} />
|
||||
<CopyRouteButton className="ms-auto size-6 p-1.5" route={route} />
|
||||
</div>
|
||||
<div className="prose-no-margin">{props.children}</div>
|
||||
<ServerSelect />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export function Property({
|
||||
children,
|
||||
}: PropertyProps) {
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border bg-fd-card p-3 prose-no-margin">
|
||||
<div className="mb-4 rounded-xl border bg-fd-card p-3 prose-no-margin">
|
||||
<h4 className="flex flex-row flex-wrap items-center gap-4">
|
||||
<code>{name}</code>
|
||||
{required ? (
|
||||
@@ -128,8 +129,6 @@ export function ObjectCollapsible(props: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Accordions type="single">
|
||||
<Accordion title={props.name}>{props.children}</Accordion>
|
||||
</Accordions>
|
||||
<CollapsiblePanel title={props.name}>{props.children}</CollapsiblePanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,8 @@ import {
|
||||
useState,
|
||||
useEffect,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from 'fumadocs-ui/components/ui/collapsible';
|
||||
import { cn, buttonVariants } from 'fumadocs-ui/components/api';
|
||||
import type {
|
||||
FieldPath,
|
||||
@@ -39,7 +33,7 @@ import { type DynamicField, SchemaContext } from '../contexts/schema';
|
||||
import { getStatusInfo } from '@/ui/playground/status-info';
|
||||
import { getUrl } from '@/utils/server-url';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { CollapsiblePanel } from '@/ui/components/collapsible';
|
||||
|
||||
interface FormValues {
|
||||
authorization: string;
|
||||
@@ -191,21 +185,12 @@ export function APIPlayground({
|
||||
<form
|
||||
{...props}
|
||||
className={cn(
|
||||
'not-prose flex flex-col gap-5 rounded-xl border bg-fd-card p-3',
|
||||
'not-prose flex flex-col gap-2 rounded-xl border bg-fd-card p-3',
|
||||
props.className,
|
||||
)}
|
||||
onSubmit={onSubmit as React.FormEventHandler}
|
||||
>
|
||||
<div className="flex flex-row gap-2">
|
||||
<RouteDisplay route={route} />
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(buttonVariants({ color: 'secondary' }))}
|
||||
disabled={testQuery.isLoading}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<FormHeader route={route} isLoading={testQuery.isLoading} />
|
||||
|
||||
{authorization
|
||||
? renderCustomField('authorization', authorization, fields.auth)
|
||||
@@ -266,26 +251,6 @@ export function APIPlayground({
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsiblePanel({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Collapsible className="-m-2">
|
||||
<CollapsibleTrigger className="group flex w-full flex-row items-center justify-between p-2 font-medium">
|
||||
{title}
|
||||
<ChevronDown className="size-4 group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="flex flex-col gap-4 p-2">{children}</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
function createPathnameFromInput(
|
||||
route: string,
|
||||
path: Record<string, unknown>,
|
||||
@@ -312,7 +277,13 @@ function createPathnameFromInput(
|
||||
: pathname;
|
||||
}
|
||||
|
||||
function RouteDisplay({ route }: { route: string }): ReactElement {
|
||||
function FormHeader({
|
||||
route,
|
||||
isLoading,
|
||||
}: {
|
||||
route: string;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const [path, query] = useWatch<FormValues, ['path', 'query']>({
|
||||
name: ['path', 'query'],
|
||||
});
|
||||
@@ -323,9 +294,21 @@ function RouteDisplay({ route }: { route: string }): ReactElement {
|
||||
);
|
||||
|
||||
return (
|
||||
<code className="flex-1 overflow-auto text-nowrap rounded-lg border bg-fd-secondary px-2 py-1.5 text-sm text-fd-secondary-foreground">
|
||||
{pathname}
|
||||
</code>
|
||||
<div className="flex flex-row gap-2 mb-2">
|
||||
<code className="inline-flex flex-row items-center rounded-full flex-1 overflow-auto text-nowrap border px-2 text-xs text-fd-secondary-foreground bg-fd-secondary">
|
||||
{pathname}
|
||||
</code>
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(
|
||||
buttonVariants({ color: 'primary' }),
|
||||
'px-3 py-1.5 rounded-full',
|
||||
)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ function InputContainer({
|
||||
}: InputContainerProps): React.ReactElement {
|
||||
return (
|
||||
<div {...props} className={cn('flex flex-col gap-1', props.className)}>
|
||||
<div className={cn(labelVariants(), 'inline-flex items-center gap-1')}>
|
||||
{name}
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<span className={cn(labelVariants())}>{name}</span>
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
<div className="flex-1" />
|
||||
{type ? (
|
||||
@@ -402,7 +402,7 @@ export function InputField({
|
||||
header={
|
||||
<>
|
||||
<FormLabel className="inline-flex items-center gap-1">
|
||||
{name}
|
||||
<span className={cn(labelVariants())}>{name}</span>
|
||||
{field.isRequired ? <span className="text-red-500">*</span> : null}
|
||||
<code className="ms-auto text-xs text-fd-muted-foreground">
|
||||
{field.type}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
import { cn } from 'fumadocs-ui/components/api';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { useApiClientModal } from '@scalar/api-client-react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'rounded-full border inline-flex flex-row items-center py-1.5 px-2 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
export default function ScalarPlayground({
|
||||
path,
|
||||
method,
|
||||
}: {
|
||||
path: string;
|
||||
method: string;
|
||||
}) {
|
||||
const client = useApiClientModal();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-2 p-3 rounded-xl border bg-fd-card text-fd-card-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
badgeVariants({
|
||||
color: getBadgeColor(method),
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{method.toUpperCase()}
|
||||
</span>
|
||||
<code className="inline-flex flex-row items-center rounded-full flex-1 overflow-auto text-nowrap border px-2 text-xs text-fd-secondary-foreground bg-fd-secondary">
|
||||
{path}
|
||||
</code>
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(
|
||||
buttonVariants({ color: 'primary' }),
|
||||
'px-3 py-1.5 rounded-full',
|
||||
)}
|
||||
onClick={() => client?.open({ path, method })}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
import { ApiClientModalProvider } from '@scalar/api-client-react';
|
||||
import { type ReactNode, useEffect, useState } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
export default function ScalarProvider({
|
||||
spec,
|
||||
children,
|
||||
}: {
|
||||
spec: object;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={mounted ? `${resolvedTheme}-mode` : ''}>
|
||||
<ApiClientModalProvider
|
||||
configuration={{
|
||||
themeId: 'moon',
|
||||
spec: {
|
||||
content: spec,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApiClientModalProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
'use client';
|
||||
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from 'fumadocs-ui/components/ui/collapsible';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -14,6 +8,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import { Input } from '@/ui/components/input';
|
||||
import { CollapsiblePanel } from '@/ui/components/collapsible';
|
||||
|
||||
export default function ServerSelect() {
|
||||
const { servers } = useApiContext();
|
||||
@@ -26,81 +21,73 @@ export default function ServerSelect() {
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Collapsible className="-m-2 mt-2">
|
||||
<CollapsibleTrigger className="flex w-full flex-row items-center justify-between p-2 text-xs font-medium text-fd-muted-foreground">
|
||||
Configure Server
|
||||
<ChevronDown className="size-4" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
<Select value={server?.url} onValueChange={setServer}>
|
||||
<SelectTrigger className="h-auto break-all">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{servers.map((item) => (
|
||||
<SelectItem key={item.url} value={item.url}>
|
||||
{item.url}
|
||||
<p className="text-start text-xs text-fd-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{Object.entries(schema?.variables ?? {}).map(([key, variable]) => {
|
||||
if (!server) return;
|
||||
const id = `fd_server_select_${key}`;
|
||||
<CollapsiblePanel title="Configure Server" className="mt-2">
|
||||
<Select value={server?.url} onValueChange={setServer}>
|
||||
<SelectTrigger className="h-auto break-all">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{servers.map((item) => (
|
||||
<SelectItem key={item.url} value={item.url}>
|
||||
{item.url}
|
||||
<p className="text-start text-xs text-fd-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{Object.entries(schema?.variables ?? {}).map(([key, variable]) => {
|
||||
if (!server) return;
|
||||
const id = `fd_server_select_${key}`;
|
||||
|
||||
return (
|
||||
<fieldset key={key} className="flex flex-col gap-1">
|
||||
<label
|
||||
className="font-mono text-xs text-fd-foreground"
|
||||
htmlFor={id}
|
||||
>
|
||||
{key}
|
||||
</label>
|
||||
<p className="text-xs text-fd-muted-foreground empty:hidden">
|
||||
{variable.description}
|
||||
</p>
|
||||
{variable.enum ? (
|
||||
<Select
|
||||
value={server.variables[key]}
|
||||
onValueChange={(v) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: v,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{variable.enum.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
value={server.variables[key]}
|
||||
onChange={(e) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
return (
|
||||
<fieldset key={key} className="flex flex-col gap-1">
|
||||
<label
|
||||
className="font-mono text-xs text-fd-foreground"
|
||||
htmlFor={id}
|
||||
>
|
||||
{key}
|
||||
</label>
|
||||
<p className="text-xs text-fd-muted-foreground empty:hidden">
|
||||
{variable.description}
|
||||
</p>
|
||||
{variable.enum ? (
|
||||
<Select
|
||||
value={server.variables[key]}
|
||||
onValueChange={(v) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: v,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id={id}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{variable.enum.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
value={server.variables[key]}
|
||||
onChange={(e) =>
|
||||
setServerVariables({
|
||||
...server?.variables,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
</CollapsiblePanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type DocumentInput = string | OpenAPIV3_1.Document | OpenAPIV3.Document;
|
||||
export type ProcessedDocument = {
|
||||
document: NoReference<Document>;
|
||||
dereferenceMap: DereferenceMap;
|
||||
downloaded: Document;
|
||||
};
|
||||
|
||||
const cache = new Map<string, ProcessedDocument>();
|
||||
@@ -42,6 +43,7 @@ export async function processDocument(
|
||||
const processed: ProcessedDocument = {
|
||||
document: dereferenced as NoReference<Document>,
|
||||
dereferenceMap,
|
||||
downloaded: loaded.specification as Document,
|
||||
};
|
||||
|
||||
if (!disableCache && typeof document === 'string') {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-fd-background: hsl(0, 0%, 8.04%);
|
||||
--color-fd-background: hsl(0, 0%, 7.04%);
|
||||
--color-fd-foreground: hsl(0, 0%, 92%);
|
||||
--color-fd-muted: hsl(0, 0%, 12.9%);
|
||||
--color-fd-muted-foreground: hsl(0, 0%, 60.9%);
|
||||
|
||||
@@ -5,6 +5,8 @@ export const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
color: {
|
||||
primary:
|
||||
'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
|
||||
outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
secondary:
|
||||
|
||||
Generated
+1345
-2
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user