OpenAPI: Improve UI

This commit is contained in:
Fuma Nama
2024-10-27 16:39:18 +08:00
parent 4a643ff759
commit d50750b2f4
10 changed files with 56 additions and 118 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Improved UI design
+4 -17
View File
@@ -1,10 +1,7 @@
import { Fragment, type HTMLAttributes } from 'react';
import { type HTMLAttributes } from 'react';
import * as Base from 'fumadocs-ui/components/codeblock';
import { codeToHast } from 'shiki';
import { type Jsx, toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { jsx, jsxs } from 'react/jsx-runtime';
import { sharedTransformers } from '@/utils/shiki';
import type { RenderContext } from '@/types';
import { highlight } from 'fumadocs-core/server';
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
code: string;
@@ -18,23 +15,13 @@ export async function CodeBlock({
options,
...rest
}: CodeBlockProps): Promise<React.ReactElement> {
const html = await codeToHast(code, {
const rendered = await highlight(code, {
lang,
defaultColor: false,
themes: { light: 'github-light', dark: 'github-dark' },
transformers: sharedTransformers,
...options,
});
const codeblock = toJsxRuntime(html, {
development: false,
jsx: jsx as Jsx,
jsxs: jsxs as Jsx,
Fragment,
components: {
pre: (props) => <Base.Pre {...props} {...rest} />,
},
});
return <Base.CodeBlock className="my-0">{codeblock}</Base.CodeBlock>;
return <Base.CodeBlock className="my-0">{rendered}</Base.CodeBlock>;
}
-2
View File
@@ -10,7 +10,6 @@ import { remark } from 'remark';
import remarkRehype from 'remark-rehype';
import { toJsxRuntime, type Jsx } from 'hast-util-to-jsx-runtime';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { sharedTransformers } from '@/utils/shiki';
const processor = remark()
.use(remarkGfm)
@@ -19,7 +18,6 @@ const processor = remark()
.use(rehypeCode, {
langs: [],
lazy: true,
transformers: sharedTransformers,
} satisfies Partial<RehypeCodeOptions>);
export async function Markdown({
+5 -1
View File
@@ -65,7 +65,11 @@ export function CopyRouteButton({
aria-label="Copy route path"
{...props}
>
{checked ? <Check className="size-3" /> : <Copy className="size-3" />}
{checked ? (
<Check className="size-full" />
) : (
<Copy className="size-full" />
)}
</button>
);
}
@@ -1,8 +1,7 @@
import { Fragment, type ReactNode, useEffect, useState } from 'react';
import { type ReactNode } from 'react';
import * as Base from 'fumadocs-ui/components/codeblock';
import { useApiContext } from '@/ui/contexts/api';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useShiki } from 'fumadocs-core/utils/use-shiki';
export type CodeBlockProps = {
code: string;
@@ -10,30 +9,19 @@ export type CodeBlockProps = {
};
export function CodeBlock({ code, lang = 'json' }: CodeBlockProps): ReactNode {
const { highlight } = useApiContext();
const [rendered, setRendered] = useState<ReactNode>(
<Base.Pre className="max-h-[288px]">{code}</Base.Pre>,
);
const { shikiOptions } = useApiContext();
useEffect(() => {
void highlight(lang, code).then((res) => {
const output = toJsxRuntime(res, {
jsx,
jsxs,
development: false,
Fragment,
components: {
pre: (props) => (
<Base.Pre className="max-h-[288px]" {...props}>
{props.children}
</Base.Pre>
),
},
});
setRendered(output);
});
}, [code, highlight, lang]);
const rendered = useShiki(code, {
lang,
...shikiOptions,
components: {
pre: (props) => (
<Base.Pre className="max-h-[288px]" {...props}>
{props.children}
</Base.Pre>
),
},
});
return <Base.CodeBlock className="my-0">{rendered}</Base.CodeBlock>;
}
@@ -1,18 +1,15 @@
import { cva, type VariantProps } from 'class-variance-authority';
export const badgeVariants = cva(
'rounded border px-1.5 py-1 text-xs font-medium leading-[12px]',
'rounded-xl border px-1.5 py-1 text-xs font-medium leading-[12px]',
{
variants: {
color: {
green:
'border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400',
yellow:
'border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400',
red: 'border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400',
blue: 'border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400',
orange:
'border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400',
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',
},
},
},
+2 -16
View File
@@ -8,7 +8,6 @@ import {
} from 'react';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
import type { RenderContext } from '@/types';
import { sharedTransformers } from '@/utils/shiki';
export interface ApiProviderProps {
/**
@@ -23,10 +22,7 @@ export interface ApiProviderProps {
interface ApiContextType {
baseUrl?: string;
setBaseUrl: (value: string) => void;
highlight: (
lang: string,
code: string,
) => ReturnType<(typeof import('shiki/bundle/web'))['codeToHast']>;
shikiOptions: RenderContext['shikiOptions'];
}
const ApiContext = createContext<ApiContextType | undefined>(undefined);
@@ -59,17 +55,7 @@ export function ApiProvider({
() => ({
baseUrl,
setBaseUrl,
highlight: async (lang, code) => {
const { codeToHast } = await import('shiki/bundle/web');
return codeToHast(code, {
lang,
themes: { light: 'github-light', dark: 'github-dark' },
transformers: sharedTransformers,
defaultColor: false,
...shikiOptions,
});
},
shikiOptions,
}),
[baseUrl, shikiOptions],
)}
+17 -28
View File
@@ -11,14 +11,14 @@ function Route({ route }: { route: string }): ReactNode {
const segments = route.split('/').filter((part) => part.length > 0);
return (
<div className="flex flex-row flex-wrap items-center gap-1 text-sm">
<div className="not-prose flex flex-row items-center gap-1 overflow-auto text-xs">
{segments.map((part, index) => (
<Fragment key={index}>
<span className="text-fd-muted-foreground">/</span>
{part.startsWith('{') && part.endsWith('}') ? (
<code className="text-fd-primary">{part}</code>
<code className="bg-fd-primary/10 text-fd-primary">{part}</code>
) : (
<span className="text-fd-foreground">{part}</span>
<code className="text-fd-foreground">{part}</code>
)}
</Fragment>
))}
@@ -39,17 +39,7 @@ export function APIInfo({
}): React.ReactElement {
return (
<div className={cn('min-w-0 flex-1', className)} {...props}>
<div
className={cn(
'sticky top-[calc(var(--fd-api-info-top)+36px)] z-20 mb-4 flex flex-row items-center gap-2 rounded-lg border bg-fd-card px-3 py-2 md:top-[var(--fd-api-info-top)]',
)}
style={
{
'--fd-api-info-top':
'calc(var(--fd-nav-height) + var(--fd-banner-height) + 4px)',
} as object
}
>
<div className="sticky top-[var(--fd-api-info-top)] z-[4] mb-4 flex flex-row items-center gap-1.5 border-b border-fd-foreground/10 bg-fd-card/50 px-4 py-1.5 shadow-lg backdrop-blur-lg max-lg:-mx-3 max-md:-mx-4 md:rounded-full md:border md:px-1.5">
<span
className={cn(
badgeVariants({ color: getBadgeColor(method) }),
@@ -60,7 +50,7 @@ export function APIInfo({
</span>
<Route route={route} />
<CopyRouteButton className="ms-auto size-6 p-1" route={route} />
<CopyRouteButton className="ms-auto size-6 p-1.5" route={route} />
</div>
<div className="prose-no-margin">{children}</div>
@@ -69,17 +59,23 @@ export function APIInfo({
}
export function API({
className,
children,
...props
}: HTMLAttributes<HTMLDivElement>): React.ReactElement {
return (
<div
className={cn(
'flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start',
className,
)}
{...props}
className={cn(
'flex flex-col gap-x-6 gap-y-4 max-lg:[--fd-toc-height:46px] max-md:[--fd-toc-height:36px] xl:flex-row xl:items-start',
props.className,
)}
style={
{
'--fd-api-info-top':
'calc(var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-toc-height, 0.5rem))',
...props.style,
} as object
}
>
{children}
</div>
@@ -95,7 +91,7 @@ export function Property({
}: PropertyProps): React.ReactElement {
return (
<div className="mb-4 rounded-lg border bg-fd-card p-3 prose-no-margin">
<h4 className="flex flex-row items-center gap-4">
<h4 className="flex flex-row flex-wrap items-center gap-4">
<code>{name}</code>
{required ? (
<div className={cn(badgeVariants({ color: 'red' }))}>Required</div>
@@ -124,13 +120,6 @@ export function APIExample(
'prose-no-margin md:sticky md:top-[var(--fd-api-info-top)] xl:w-[400px]',
props.className,
)}
style={
{
'--fd-api-info-top':
'calc(var(--fd-nav-height) + var(--fd-banner-height) + 40px)',
...props.style,
} as object
}
>
{props.children}
</div>
+3 -3
View File
@@ -182,14 +182,14 @@ export function APIPlayground({
)}
>
<form
className="not-prose flex flex-col gap-5 rounded-lg border bg-fd-card p-3"
className="not-prose flex flex-col gap-5 rounded-xl border bg-fd-card p-3"
onSubmit={onSubmit as React.FormEventHandler}
>
<div className="flex flex-row gap-2">
<RouteDisplay route={route} />
<button
type="submit"
className={cn(buttonVariants({ color: 'secondary' }))}
className={cn(buttonVariants({ color: 'outline' }))}
disabled={testQuery.isLoading}
>
Send
@@ -202,7 +202,7 @@ export function APIPlayground({
<Accordions
type="multiple"
className={cn(
'-m-3 border-0 text-sm',
'-m-3 border-0 bg-transparent text-sm',
path.length === 0 &&
query.length === 0 &&
header.length === 0 &&
-16
View File
@@ -1,16 +0,0 @@
import { type ShikiTransformer } from 'shiki';
export const sharedTransformers: ShikiTransformer[] = [
{
name: 'fumadocs:pre-process',
line(hast) {
if (hast.children.length === 0) {
// Keep the empty lines when using grid layout
hast.children.push({
type: 'text',
value: ' ',
});
}
},
},
];