mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Improve OpenAPI codesample (#3090)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve OpenAPI codesample (add OpenAPISelect component)
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
/* Method Tags */
|
||||
.openapi-method {
|
||||
@apply rounded uppercase font-mono font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
|
||||
@apply rounded uppercase font-mono shrink-0 font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
|
||||
}
|
||||
|
||||
.openapi-method-get {
|
||||
@@ -423,8 +423,20 @@
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
.openapi-codesample-header .openapi-select > button {
|
||||
@apply border-none;
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content {
|
||||
@apply flex flex-row items-center h-fit;
|
||||
@apply flex flex-row items-center justify-between h-fit p-2.5;
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content .openapi-path {
|
||||
@apply flex items-center font-mono *:text-[0.813rem] gap-2 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content .openapi-path .openapi-path-variable {
|
||||
@apply text-[0.813rem];
|
||||
}
|
||||
|
||||
.openapi-codesample-footer {
|
||||
@@ -437,7 +449,7 @@
|
||||
|
||||
/* Path */
|
||||
.openapi-path {
|
||||
@apply flex items-start text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full;
|
||||
@apply flex items-center text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
@@ -451,12 +463,12 @@
|
||||
}
|
||||
|
||||
.openapi-path .openapi-method {
|
||||
@apply text-[0.813rem] m-0 mt-0.5 items-center flex px-2;
|
||||
@apply text-[0.813rem] m-0 mt-0.5 items-center flex px-1;
|
||||
}
|
||||
|
||||
.openapi-path-title {
|
||||
@apply flex-1 relative font-normal text-left font-mono text-tint-strong/10;
|
||||
@apply py-0.5 px-1 rounded hover:bg-tint cursor-pointer transition-colors;
|
||||
@apply py-0.5 px-1 rounded hover:bg-tint transition-colors;
|
||||
@apply whitespace-nowrap md:whitespace-normal;
|
||||
}
|
||||
|
||||
@@ -468,14 +480,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* .openapi-path-copy {
|
||||
@apply absolute opacity-0 h-fit right-0 top-1/2 -translate-y-1/2 bg-light dark:bg-dark border rounded-md border-tint-subtle px-1.5 py-0;
|
||||
}
|
||||
|
||||
.openapi-path-title:hover .openapi-path-copy {
|
||||
@apply opacity-11;
|
||||
} */
|
||||
|
||||
.openapi-path-title em {
|
||||
@apply not-italic text-primary font-medium;
|
||||
}
|
||||
@@ -502,7 +506,8 @@
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
.openapi-panel-footer {
|
||||
.openapi-panel-footer,
|
||||
.openapi-codesample-footer {
|
||||
@apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint;
|
||||
}
|
||||
|
||||
@@ -517,7 +522,57 @@
|
||||
|
||||
/* Common Elements */
|
||||
.openapi-select {
|
||||
@apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 truncate border border-tint-subtle bg-tint;
|
||||
/* unstyled */
|
||||
}
|
||||
|
||||
/* Prevent react-aria popover from setting overflow:auto on body */
|
||||
body:has(.openapi-select-popover) {
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
.openapi-select > button {
|
||||
@apply flex items-center cursor-pointer gap-1.5 text-tint-strong max-w-60 rounded text-xs leading-6 px-1.5 truncate border border-tint-subtle bg-tint;
|
||||
@apply hover:bg-tint-hover transition-all;
|
||||
}
|
||||
|
||||
.openapi-select > button > span.react-aria-SelectValue {
|
||||
@apply shrink truncate;
|
||||
}
|
||||
|
||||
.openapi-select > button > .gb-icon {
|
||||
@apply shrink-0;
|
||||
}
|
||||
|
||||
.openapi-select > button svg {
|
||||
@apply size-2.5;
|
||||
}
|
||||
|
||||
.openapi-select-popover {
|
||||
@apply min-w-32 max-w-fit w-auto max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md;
|
||||
@apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
|
||||
}
|
||||
|
||||
.openapi-select-popover[data-entering] {
|
||||
animation: popover-enter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.openapi-select-popover[data-exiting] {
|
||||
animation: popover-leave 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.openapi-select-item {
|
||||
@apply text-sm cursor-pointer px-1.5 py-0.5 truncate text-tint ring-0 border-none rounded !outline-none;
|
||||
@apply hover:bg-tint-hover theme-gradient:hover:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
||||
}
|
||||
|
||||
.openapi-select-item-selected {
|
||||
@apply text-primary-subtle hover:text-primary hover:bg-primary-hover;
|
||||
@apply theme-muted:hover:bg-primary-active theme-gradient:hover:bg-primary-active tint:font-semibold;
|
||||
@apply contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:font-semibold;
|
||||
}
|
||||
|
||||
.openapi-select-listbox {
|
||||
@apply flex flex-col gap-1 focus:ring-0 focus:outline-none;
|
||||
}
|
||||
|
||||
.openapi-select:focus {
|
||||
@@ -580,8 +635,10 @@
|
||||
@apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
|
||||
}
|
||||
|
||||
.openapi-tabs-panel {
|
||||
.openapi-tabs-panel,
|
||||
.openapi-codesample-panel {
|
||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
/* Disclosure group */
|
||||
@@ -731,6 +788,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-copy-button {
|
||||
@apply hover:brightness-95;
|
||||
@keyframes popover-enter {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes popover-leave {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-copy-button {
|
||||
@apply hover:brightness-95 cursor-pointer;
|
||||
}
|
||||
|
||||
.openapi-copy-button[data-disabled="true"] {
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import clsx from 'clsx';
|
||||
import { useRef, useState } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
import { useDisclosureState } from 'react-stately';
|
||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
||||
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
|
||||
|
||||
interface InteractiveSectionTab {
|
||||
@@ -106,24 +107,25 @@ export function InteractiveSection(props: {
|
||||
}}
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<select
|
||||
<OpenAPISelect
|
||||
className={clsx(
|
||||
'openapi-section-select',
|
||||
'openapi-select',
|
||||
`${className}-tabs-select`
|
||||
)}
|
||||
value={selectedTab?.key ?? ''}
|
||||
onChange={(event) => {
|
||||
setSelectedTab(event.target.value);
|
||||
items={tabs}
|
||||
selectedKey={selectedTab?.key ?? ''}
|
||||
onSelectionChange={(key) => {
|
||||
setSelectedTab(String(key));
|
||||
state.expand();
|
||||
}}
|
||||
placement="bottom end"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.key} value={tab.key}>
|
||||
<OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
|
||||
{tab.label}
|
||||
</option>
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</select>
|
||||
</OpenAPISelect>
|
||||
) : null}
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
@@ -3,15 +3,14 @@ import {
|
||||
OpenAPIMediaTypeExamplesBody,
|
||||
OpenAPIMediaTypeExamplesSelector,
|
||||
} from './OpenAPICodeSampleInteractive';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector';
|
||||
import { ScalarApiButton } from './ScalarApiButton';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
|
||||
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
import { checkIsReference, createStateKey } from './utils';
|
||||
import { checkIsReference } from './utils';
|
||||
|
||||
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
|
||||
|
||||
@@ -44,13 +43,7 @@ export function OpenAPICodeSample(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
return <OpenAPICodeSampleBody data={data} items={samples} />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,8 @@ import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
||||
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
||||
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||
|
||||
type MediaTypeState = {
|
||||
mediaType: string;
|
||||
@@ -15,27 +16,27 @@ function useMediaTypeState(
|
||||
defaultKey: string
|
||||
): MediaTypeState {
|
||||
const { method, path } = data;
|
||||
const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey));
|
||||
if (typeof store.tabKey !== 'string') {
|
||||
const store = useStore(getOrCreateStoreByKey(`media-type-${method}-${path}`, defaultKey));
|
||||
if (typeof store.key !== 'string') {
|
||||
throw new Error('Media type key is not a string');
|
||||
}
|
||||
return {
|
||||
mediaType: store.tabKey,
|
||||
setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]),
|
||||
mediaType: store.key,
|
||||
setMediaType: useCallback((index: string) => store.setKey(index), [store.setKey]),
|
||||
};
|
||||
}
|
||||
|
||||
function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) {
|
||||
const { method, path } = data;
|
||||
const store = useStore(
|
||||
getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
|
||||
getOrCreateStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
|
||||
);
|
||||
if (typeof store.tabKey !== 'number') {
|
||||
if (typeof store.key !== 'number') {
|
||||
throw new Error('Example key is not a number');
|
||||
}
|
||||
return {
|
||||
index: store.tabKey,
|
||||
setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]),
|
||||
index: store.key,
|
||||
setIndex: useCallback((index: number) => store.setKey(index), [store.setKey]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,18 +70,28 @@ function MediaTypeSelector(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = renderers.map((renderer) => ({
|
||||
key: renderer.mediaType,
|
||||
label: renderer.mediaType,
|
||||
}));
|
||||
|
||||
return (
|
||||
<select
|
||||
<OpenAPISelect
|
||||
className={clsx('openapi-select')}
|
||||
value={state.mediaType}
|
||||
onChange={(e) => state.setMediaType(e.target.value)}
|
||||
selectedKey={state.mediaType}
|
||||
items={renderers.map((renderer) => ({
|
||||
key: renderer.mediaType,
|
||||
label: renderer.mediaType,
|
||||
}))}
|
||||
onSelectionChange={(e) => state.setMediaType(String(e))}
|
||||
placement="bottom start"
|
||||
>
|
||||
{renderers.map((renderer) => (
|
||||
<option key={renderer.mediaType} value={renderer.mediaType}>
|
||||
{renderer.mediaType}
|
||||
</option>
|
||||
{items.map((item) => (
|
||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||
{item.label}
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</select>
|
||||
</OpenAPISelect>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,18 +106,24 @@ function ExamplesSelector(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = renderer.examples.map((example, index) => ({
|
||||
key: index,
|
||||
label: example.example.summary || `Example ${index + 1}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<select
|
||||
className={clsx('openapi-select')}
|
||||
value={String(state.index)}
|
||||
onChange={(e) => state.setIndex(Number(e.target.value))}
|
||||
<OpenAPISelect
|
||||
items={items}
|
||||
selectedKey={state.index}
|
||||
onSelectionChange={(e) => state.setIndex(Number(e))}
|
||||
placement="bottom start"
|
||||
>
|
||||
{renderer.examples.map((example, index) => (
|
||||
<option key={index} value={index}>
|
||||
{example.example.summary || `Example ${index + 1}`}
|
||||
</option>
|
||||
{items.map((item) => (
|
||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||
{item.label}
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</select>
|
||||
</OpenAPISelect>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import type { Key } from 'react-aria';
|
||||
import { useStore } from 'zustand';
|
||||
import { OpenAPIPath } from './OpenAPIPath';
|
||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
|
||||
function useCodeSampleState(initialKey: Key = 'default') {
|
||||
const store = useStore(getOrCreateStoreByKey('codesample', initialKey));
|
||||
return {
|
||||
key: store.key,
|
||||
setKey: useCallback((key: Key) => store.setKey(key), [store.setKey]),
|
||||
};
|
||||
}
|
||||
|
||||
type CodeSampleItem = OpenAPISelectItem & {
|
||||
body: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function OpenAPICodeSampleHeader(props: {
|
||||
items: CodeSampleItem[];
|
||||
data: OpenAPIOperationData;
|
||||
}) {
|
||||
const { data, items } = props;
|
||||
|
||||
const state = useCodeSampleState(items[0]?.key ?? '');
|
||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<OpenAPIPath canCopy={false} withServer={false} data={data} />
|
||||
{items.length > 1 ? (
|
||||
<OpenAPISelect
|
||||
selectedKey={selected?.key}
|
||||
onSelectionChange={(key) => {
|
||||
state.setKey(key);
|
||||
}}
|
||||
items={items}
|
||||
placement="bottom end"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||
{item.label}
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</OpenAPISelect>
|
||||
) : items[0] ? (
|
||||
<span className="openapi-codesample-label">{items[0].label}</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenAPICodeSampleBody(props: {
|
||||
items: CodeSampleItem[];
|
||||
data: OpenAPIOperationData;
|
||||
}) {
|
||||
const { items, data } = props;
|
||||
if (!items[0]) {
|
||||
throw new Error('No items provided');
|
||||
}
|
||||
|
||||
const state = useCodeSampleState(items[0]?.key);
|
||||
|
||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
||||
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StaticSection
|
||||
header={<OpenAPICodeSampleHeader data={data} items={items} />}
|
||||
className="openapi-codesample"
|
||||
>
|
||||
<div id={selected.key as string} className="openapi-codesample-panel">
|
||||
{selected.body ? selected.body : null}
|
||||
{selected.footer ? selected.footer : null}
|
||||
</div>
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export function OpenAPIOperation(props: {
|
||||
title: operation.summary,
|
||||
})
|
||||
: null}
|
||||
<OpenAPIPath data={data} context={context} />
|
||||
<OpenAPIPath data={data} />
|
||||
</div>
|
||||
<div className="openapi-columns">
|
||||
<div className="openapi-column-spec">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
|
||||
/**
|
||||
@@ -7,25 +7,42 @@ import { getDefaultServerURL } from './util/server';
|
||||
*/
|
||||
export function OpenAPIPath(props: {
|
||||
data: OpenAPIOperationData;
|
||||
context: OpenAPIContext;
|
||||
/** Whether to show the server URL.
|
||||
* @default true
|
||||
*/
|
||||
withServer?: boolean;
|
||||
/**
|
||||
* Whether the path is copyable.
|
||||
* @default true
|
||||
*/
|
||||
canCopy?: boolean;
|
||||
}) {
|
||||
const { data } = props;
|
||||
const { data, withServer = true, canCopy = true } = props;
|
||||
const { method, path, operation } = data;
|
||||
|
||||
const server = getDefaultServerURL(data.servers);
|
||||
const formattedPath = formatPath(path);
|
||||
|
||||
const element = (() => {
|
||||
return (
|
||||
<>
|
||||
{withServer ? <span className="openapi-path-server">{server}</span> : null}
|
||||
{formattedPath}
|
||||
</>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="openapi-path">
|
||||
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
||||
|
||||
<OpenAPICopyButton
|
||||
value={server + path}
|
||||
value={`${withServer ? server : ''}${path}`}
|
||||
className="openapi-path-title"
|
||||
data-deprecated={operation.deprecated}
|
||||
isDisabled={!canCopy}
|
||||
>
|
||||
<span className="openapi-path-server">{server}</span>
|
||||
{formattedPath}
|
||||
{element}
|
||||
</OpenAPICopyButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
Button,
|
||||
type Key,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
type ListBoxItemProps,
|
||||
Popover,
|
||||
type PopoverProps,
|
||||
Select,
|
||||
type SelectProps,
|
||||
SelectValue,
|
||||
} from 'react-aria-components';
|
||||
|
||||
export type OpenAPISelectItem = {
|
||||
key: Key;
|
||||
label: string;
|
||||
};
|
||||
|
||||
interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectProps<T>, 'children'> {
|
||||
items: T[];
|
||||
children: React.ReactNode | ((item: T) => React.ReactNode);
|
||||
selectedKey?: Key;
|
||||
onChange?: (key: string | number) => void;
|
||||
placement?: PopoverProps['placement'];
|
||||
}
|
||||
|
||||
export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectProps<T>) {
|
||||
const { items, children, className, placement } = props;
|
||||
|
||||
return (
|
||||
<Select {...props} className={clsx('openapi-select', className)}>
|
||||
<Button>
|
||||
<SelectValue />
|
||||
<span aria-hidden="true">
|
||||
<svg
|
||||
className="gb-icon"
|
||||
style={{
|
||||
maskImage:
|
||||
"url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/chevron-down.svg?v=2&token=a463935e93')",
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'center center',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
<Popover placement={placement} className="openapi-select-popover">
|
||||
<ListBox className="openapi-select-listbox" items={items}>
|
||||
{children}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenAPISelectItem(props: ListBoxItemProps) {
|
||||
return (
|
||||
<ListBoxItem
|
||||
{...props}
|
||||
className={({ isFocused, isSelected }) =>
|
||||
clsx('openapi-select-item', {
|
||||
'openapi-select-item-focused': isFocused,
|
||||
'openapi-select-item-selected': isSelected,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
||||
import { useEventCallback } from 'usehooks-ts';
|
||||
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
||||
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||
|
||||
export type TabItem = {
|
||||
key: Key;
|
||||
@@ -36,8 +36,8 @@ export function OpenAPITabs(
|
||||
const { children, items, stateKey } = props;
|
||||
const [tabKey, setTabKey] = useState<Key | null>(() => {
|
||||
if (stateKey && typeof window !== 'undefined') {
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
const tabKey = store.getState().tabKey;
|
||||
const store = getOrCreateStoreByKey(stateKey);
|
||||
const tabKey = store.getState().key;
|
||||
if (tabKey) {
|
||||
return tabKey;
|
||||
}
|
||||
@@ -60,10 +60,10 @@ export function OpenAPITabs(
|
||||
if (!stateKey) {
|
||||
return undefined;
|
||||
}
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
const store = getOrCreateStoreByKey(stateKey);
|
||||
return store.subscribe((state) => {
|
||||
cancelDeferRef.current?.();
|
||||
cancelDeferRef.current = defer(() => selectTab(state.tabKey));
|
||||
cancelDeferRef.current = defer(() => selectTab(state.key));
|
||||
});
|
||||
}, [stateKey, selectTab]);
|
||||
useEffect(() => {
|
||||
@@ -77,8 +77,8 @@ export function OpenAPITabs(
|
||||
onSelectionChange={(tabKey) => {
|
||||
selectTab(tabKey);
|
||||
if (stateKey) {
|
||||
const store = getOrCreateTabStoreByKey(stateKey);
|
||||
store.setState({ tabKey });
|
||||
const store = getOrCreateStoreByKey(stateKey);
|
||||
store.setState({ key: tabKey });
|
||||
}
|
||||
}}
|
||||
selectedKey={tabKey}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { createStore } from 'zustand';
|
||||
|
||||
type Key = string | number;
|
||||
|
||||
type State = {
|
||||
key: Key | null;
|
||||
};
|
||||
|
||||
type Actions = { setKey: (key: Key | null) => void };
|
||||
|
||||
type Store = State & Actions;
|
||||
|
||||
const createStateStore = (initial?: Key) => {
|
||||
return createStore<Store>()((set) => ({
|
||||
key: initial ?? null,
|
||||
setKey: (key) => {
|
||||
set(() => ({ key }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const defaultStores = new Map<string, ReturnType<typeof createStateStore>>();
|
||||
|
||||
const createStateStoreFactory = (stores: typeof defaultStores) => {
|
||||
return (storeKey: string, initialKey?: Key) => {
|
||||
if (!stores.has(storeKey)) {
|
||||
stores.set(storeKey, createStateStore(initialKey));
|
||||
}
|
||||
return stores.get(storeKey)!;
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrCreateStoreByKey = createStateStoreFactory(defaultStores);
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createStore } from 'zustand';
|
||||
|
||||
type Key = string | number;
|
||||
|
||||
type TabState = {
|
||||
tabKey: Key | null;
|
||||
};
|
||||
|
||||
type TabActions = { setTabKey: (tab: Key | null) => void };
|
||||
|
||||
type TabStore = TabState & TabActions;
|
||||
|
||||
const createTabStore = (initialTab?: Key) => {
|
||||
return createStore<TabStore>()((set) => ({
|
||||
tabKey: initialTab ?? null,
|
||||
setTabKey: (tabKey) => {
|
||||
set(() => ({ tabKey }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const defaultTabStores = new Map<string, ReturnType<typeof createTabStore>>();
|
||||
|
||||
const createTabStoreFactory = (stores: typeof defaultTabStores) => {
|
||||
return (storeKey: string, initialKey?: Key) => {
|
||||
if (!stores.has(storeKey)) {
|
||||
stores.set(storeKey, createTabStore(initialKey));
|
||||
}
|
||||
return stores.get(storeKey)!;
|
||||
};
|
||||
};
|
||||
|
||||
export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores);
|
||||
Reference in New Issue
Block a user