mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Improve UI
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Improved UI design
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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: ' ',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user