mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Update OpenAPI operation path design (#2972)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update OpenAPI operation path design
|
||||||
@@ -264,14 +264,18 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.scalar-activate-button {
|
.scalar-activate-button {
|
||||||
@apply flex gap-1.5 items-center;
|
@apply flex gap-2 items-center;
|
||||||
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
|
@apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start;
|
||||||
@apply ring-1 ring-tint hover:ring-tint-hover;
|
@apply ring-1 ring-tint hover:ring-tint-hover;
|
||||||
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
@apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none;
|
||||||
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
@apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12;
|
||||||
@apply hover:scale-105 active:scale-100 transition-all;
|
@apply hover:scale-105 active:scale-100 transition-all;
|
||||||
@apply grow-0 shrink-0 truncate;
|
@apply grow-0 shrink-0 truncate;
|
||||||
@apply text-sm px-2.5 py-1;
|
@apply text-[13px] px-2 py-0.5 font-mono font-medium [word-spacing:-2px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.scalar-activate-button svg {
|
||||||
|
@apply size-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scalar-app-loading {
|
.scalar-app-loading {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Layout Components */
|
/* Layout Components */
|
||||||
.openapi-operation {
|
.openapi-operation {
|
||||||
@apply flex-1 flex flex-col gap-4 mb-14;
|
@apply flex-1 flex flex-col gap-8 mb-14;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-schemas {
|
.openapi-schemas {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-summary {
|
.openapi-summary {
|
||||||
@apply flex flex-col items-start justify-start gap-2;
|
@apply flex flex-col items-start justify-start gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-deprecated {
|
.openapi-deprecated {
|
||||||
@@ -391,17 +391,30 @@
|
|||||||
@apply flex flex-row items-center h-fit;
|
@apply flex flex-row items-center h-fit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-codesample-footer {
|
||||||
|
@apply flex w-full justify-end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Path */
|
/* Path */
|
||||||
.openapi-path {
|
.openapi-path {
|
||||||
@apply flex items-center bg-transparent text-sm gap-2 p-2 border rounded-md border-tint-subtle;
|
@apply flex items-center text-sm gap-2 h-fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-variable {
|
||||||
|
@apply p-px min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-none before:!content-none after:!content-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-server {
|
||||||
|
@apply text-tint hidden md:inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path .openapi-method {
|
.openapi-path .openapi-method {
|
||||||
@apply text-[0.813rem] m-0 px-1;
|
@apply text-[0.813rem] m-0 h-full items-center flex px-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path-title {
|
.openapi-path-title {
|
||||||
@apply flex-1 relative font-normal whitespace-nowrap overflow-x-auto font-mono text-tint-strong;
|
@apply flex-1 relative font-normal whitespace-nowrap overflow-x-auto font-mono text-tint-strong/10;
|
||||||
|
@apply py-0.5 px-1 rounded hover:bg-tint cursor-pointer transition-colors;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
@@ -520,6 +533,10 @@
|
|||||||
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-tabs-footer .openapi-markdown {
|
||||||
|
@apply text-[0.813rem] text-tint;
|
||||||
|
}
|
||||||
|
|
||||||
/* Disclosure group */
|
/* Disclosure group */
|
||||||
.openapi-disclosure-group {
|
.openapi-disclosure-group {
|
||||||
@apply border-b border-tint-subtle relative;
|
@apply border-b border-tint-subtle relative;
|
||||||
@@ -631,3 +648,37 @@
|
|||||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
|
||||||
@apply p-2.5;
|
@apply p-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip {
|
||||||
|
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md font-medium px-1.5 py-0.5 shadow-sm text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip svg {
|
||||||
|
@apply size-3 text-tint-strong;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip[data-entering] {
|
||||||
|
animation: tooltip-enter 0.2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-tooltip[data-exiting] {
|
||||||
|
animation: tooltip-leave 0.2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-enter {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-leave {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||||
|
import { ScalarApiButton } from './ScalarApiButton';
|
||||||
import { StaticSection } from './StaticSection';
|
import { StaticSection } from './StaticSection';
|
||||||
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||||
@@ -79,6 +81,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
code: generator.generate(input),
|
code: generator.generate(input),
|
||||||
syntax: generator.syntax,
|
syntax: generator.syntax,
|
||||||
}),
|
}),
|
||||||
|
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Use custom samples if defined
|
// Use custom samples if defined
|
||||||
@@ -105,6 +108,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
code: sample.source,
|
code: sample.source,
|
||||||
syntax: sample.lang,
|
syntax: sample.lang,
|
||||||
}),
|
}),
|
||||||
|
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,6 +132,30 @@ export function OpenAPICodeSample(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function OpenAPICodeSampleFooter(props: {
|
||||||
|
data: OpenAPIOperationData;
|
||||||
|
context: OpenAPIContextProps;
|
||||||
|
}) {
|
||||||
|
const { data, context } = props;
|
||||||
|
const { method, path } = data;
|
||||||
|
const { specUrl } = context;
|
||||||
|
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
||||||
|
|
||||||
|
if (hideTryItPanel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateHttpMethod(method)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="openapi-codesample-footer">
|
||||||
|
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} {
|
} {
|
||||||
@@ -169,3 +197,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
|
||||||
|
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
|
||||||
|
|
||||||
|
export function OpenAPICopyButton(
|
||||||
|
props: ButtonProps & {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { value } = props;
|
||||||
|
const { children, onPress, className } = props;
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (!value) return;
|
||||||
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
preventFocusOnPress
|
||||||
|
onPress={(e) => {
|
||||||
|
handleCopy();
|
||||||
|
onPress?.(e);
|
||||||
|
}}
|
||||||
|
className={`openapi-copy-button ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={setIsOpen}
|
||||||
|
placement="top"
|
||||||
|
offset={4}
|
||||||
|
className="openapi-tooltip"
|
||||||
|
>
|
||||||
|
{copied ? 'Copied' : 'Copy to clipboard'}{' '}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ export function OpenAPIOperation(props: {
|
|||||||
title: operation.summary,
|
title: operation.summary,
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
<OpenAPIPath data={data} context={context} />
|
||||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="openapi-columns">
|
<div className="openapi-columns">
|
||||||
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<OpenAPIOperationDescription operation={operation} context={context} />
|
<OpenAPIOperationDescription operation={operation} context={context} />
|
||||||
<OpenAPIPath data={data} context={context} />
|
|
||||||
<OpenAPISpec data={data} context={clientContext} />
|
<OpenAPISpec data={data} context={clientContext} />
|
||||||
</div>
|
</div>
|
||||||
<div className="openapi-column-preview">
|
<div className="openapi-column-preview">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||||
import type React from 'react';
|
|
||||||
import { ScalarApiButton } from './ScalarApiButton';
|
|
||||||
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
||||||
|
import { getDefaultServerURL } from './util/server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the path of an operation.
|
* Display the path of an operation.
|
||||||
@@ -10,63 +9,62 @@ export function OpenAPIPath(props: {
|
|||||||
data: OpenAPIOperationData;
|
data: OpenAPIOperationData;
|
||||||
context: OpenAPIContextProps;
|
context: OpenAPIContextProps;
|
||||||
}) {
|
}) {
|
||||||
const { data, context } = props;
|
const { data } = props;
|
||||||
const { method, path } = data;
|
const { method, path, operation } = data;
|
||||||
const { specUrl } = context;
|
|
||||||
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
const server = getDefaultServerURL(data.servers);
|
||||||
|
const formattedPath = formatPath(path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-path">
|
<div className="openapi-path">
|
||||||
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
||||||
<div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
|
|
||||||
<p>{formatPath(path)}</p>
|
<OpenAPICopyButton
|
||||||
</div>
|
value={server + path}
|
||||||
{!hideTryItPanel && validateHttpMethod(method) && (
|
className="openapi-path-title"
|
||||||
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
data-deprecated={operation.deprecated}
|
||||||
)}
|
>
|
||||||
|
<span className="openapi-path-server">{server}</span>
|
||||||
|
{formattedPath}
|
||||||
|
</OpenAPICopyButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHttpMethod(method: string): method is OpenAPIV3_1.HttpMethods {
|
/**
|
||||||
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
* Format the path by wrapping placeholders in <span> tags.
|
||||||
}
|
*/
|
||||||
|
|
||||||
// Format the path to highlight placeholders
|
|
||||||
function formatPath(path: string) {
|
function formatPath(path: string) {
|
||||||
// Matches placeholders like {id}, {userId}, etc.
|
// Matches placeholders like {id}, {userId}, etc.
|
||||||
const regex = /\{(\w+)\}/g;
|
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
||||||
|
|
||||||
const parts: (string | React.JSX.Element)[] = [];
|
const parts: (string | React.JSX.Element)[] = [];
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
|
|
||||||
// Replace placeholders with <em> tags
|
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
||||||
path.replace(regex, (match, key, offset) => {
|
path.replace(regex, (match, _, offset) => {
|
||||||
parts.push(path.slice(lastIndex, offset));
|
if (offset > lastIndex) {
|
||||||
parts.push(<em key={key}>{`{${key}}`}</em>);
|
parts.push(path.slice(lastIndex, offset));
|
||||||
|
}
|
||||||
|
parts.push(
|
||||||
|
<span key={offset} className="openapi-path-variable">
|
||||||
|
{match}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
lastIndex = offset + match.length;
|
lastIndex = offset + match.length;
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Push remaining text after the last placeholder
|
if (lastIndex < path.length) {
|
||||||
parts.push(path.slice(lastIndex));
|
parts.push(path.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
// Join parts with separators wrapped in <span>
|
const formattedPath = parts.map((part, index) => {
|
||||||
const formattedPath = parts.reduce(
|
if (typeof part === 'string') {
|
||||||
(acc, part, index) => {
|
return <span key={index}>{part}</span>;
|
||||||
if (typeof part === 'string' && index > 0 && part === '/') {
|
}
|
||||||
acc.push(
|
return part;
|
||||||
<span className="openapi-path-separator" key={`sep-${index}`}>
|
});
|
||||||
/
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.push(part);
|
return formattedPath;
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[] as (string | React.JSX.Element)[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <span>{formattedPath}</span>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||||
import { StaticSection } from './StaticSection';
|
import { StaticSection } from './StaticSection';
|
||||||
import { generateSchemaExample } from './generateSchemaExample';
|
import { generateSchemaExample } from './generateSchemaExample';
|
||||||
@@ -39,44 +40,40 @@ export function OpenAPIResponseExample(props: {
|
|||||||
return Number(a) - Number(b);
|
return Number(a) - Number(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabs = responses
|
const tabs = responses.map(([key, responseObject]) => {
|
||||||
.map(([key, responseObject]) => {
|
const description = resolveDescription(responseObject);
|
||||||
const description = resolveDescription(responseObject);
|
|
||||||
|
|
||||||
if (checkIsReference(responseObject)) {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
label: key,
|
|
||||||
description,
|
|
||||||
body: (
|
|
||||||
<OpenAPIExample
|
|
||||||
example={getExampleFromReference(responseObject)}
|
|
||||||
context={context}
|
|
||||||
syntax="json"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
label: key,
|
|
||||||
description,
|
|
||||||
body: <OpenAPIEmptyResponseExample />,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (checkIsReference(responseObject)) {
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label: key,
|
||||||
description: resolveDescription(responseObject),
|
body: (
|
||||||
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
<OpenAPIExample
|
||||||
|
example={getExampleFromReference(responseObject)}
|
||||||
|
context={context}
|
||||||
|
syntax="json"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
.filter((val): val is { key: string; label: string; body: any; description: string } =>
|
|
||||||
Boolean(val)
|
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
||||||
);
|
return {
|
||||||
|
key: key,
|
||||||
|
label: key,
|
||||||
|
body: <OpenAPIEmptyResponseExample />,
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
label: key,
|
||||||
|
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
||||||
|
footer: description ? <Markdown source={description} /> : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (tabs.length === 0) {
|
if (tabs.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
||||||
import { useEventCallback } from 'usehooks-ts';
|
import { useEventCallback } from 'usehooks-ts';
|
||||||
import { Markdown } from './Markdown';
|
|
||||||
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
||||||
|
|
||||||
export type TabItem = {
|
export type TabItem = {
|
||||||
key: Key;
|
key: Key;
|
||||||
label: string;
|
label: string;
|
||||||
body: React.ReactNode;
|
body: React.ReactNode;
|
||||||
description?: string;
|
footer?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OpenAPITabsContextData = {
|
type OpenAPITabsContextData = {
|
||||||
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
|
|||||||
return (
|
return (
|
||||||
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
||||||
{selectedTab.body}
|
{selectedTab.body}
|
||||||
{selectedTab.description ? (
|
{selectedTab.footer ? (
|
||||||
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
<OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
|
||||||
) : null}
|
) : null}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The OpenAPI Tabs panel footer component.
|
||||||
|
* This component should be used as a child of the OpenAPITabs component.
|
||||||
|
*/
|
||||||
|
function OpenAPITabsPanelFooter(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <div className="openapi-tabs-footer">{children}</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export function ScalarApiButton(props: {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
Test it
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Test it
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen &&
|
||||||
|
|||||||
Reference in New Issue
Block a user