@redocly/theme 0.58.0-next.9 → 0.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Catalog/CatalogEntity/CatalogEntity.d.ts +5 -1
- package/lib/components/Catalog/CatalogEntity/CatalogEntity.js +4 -4
- package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +3 -3
- package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.d.ts +5 -1
- package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +9 -7
- package/lib/components/CodeBlock/CodeBlock.d.ts +5 -12
- package/lib/components/CodeBlock/CodeBlockControls.d.ts +3 -3
- package/lib/components/CodeBlock/CodeBlockControls.js +1 -1
- package/lib/components/CodeBlock/CodeBlockDropdown.d.ts +2 -2
- package/lib/components/CodeBlock/CodeBlockDropdown.js +4 -13
- package/lib/components/CodeBlock/CodeBlockTabs.d.ts +2 -2
- package/lib/components/CodeBlock/CodeBlockTabs.js +4 -3
- package/lib/components/JsonViewer/JsonViewer.d.ts +1 -1
- package/lib/components/JsonViewer/JsonViewer.js +9 -10
- package/lib/components/PageActions/PageActions.d.ts +4 -1
- package/lib/components/PageActions/PageActions.js +2 -2
- package/lib/components/Panel/variables.js +1 -0
- package/lib/core/constants/catalog.js +4 -0
- package/lib/core/contexts/CodeSnippetContext.d.ts +14 -6
- package/lib/core/contexts/CodeSnippetContext.js +57 -14
- package/lib/core/hooks/use-codeblock-tabs-controls.d.ts +2 -2
- package/lib/core/hooks/use-local-state.js +22 -18
- package/lib/core/hooks/use-page-actions.d.ts +2 -1
- package/lib/core/hooks/use-page-actions.js +48 -6
- package/lib/core/openapi/index.d.ts +1 -0
- package/lib/core/openapi/index.js +3 -1
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/open-api-server.d.ts +1 -0
- package/lib/icons/CursorIcon/CursorIcon.d.ts +9 -0
- package/lib/icons/CursorIcon/CursorIcon.js +22 -0
- package/lib/layouts/DocumentationLayout.js +1 -3
- package/lib/markdoc/components/CodeGroup/CodeGroup.js +49 -27
- package/lib/markdoc/components/Tabs/TabList.js +2 -0
- package/package.json +4 -4
- package/src/components/Catalog/CatalogEntity/CatalogEntity.tsx +15 -2
- package/src/components/Catalog/CatalogEntity/CatalogEntityMetadata.tsx +3 -3
- package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +27 -18
- package/src/components/CodeBlock/CodeBlock.tsx +5 -11
- package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
- package/src/components/CodeBlock/CodeBlockDropdown.tsx +11 -20
- package/src/components/CodeBlock/CodeBlockTabs.tsx +8 -8
- package/src/components/JsonViewer/JsonViewer.tsx +16 -9
- package/src/components/PageActions/PageActions.tsx +6 -4
- package/src/components/Panel/variables.ts +1 -0
- package/src/core/constants/catalog.ts +4 -0
- package/src/core/contexts/CodeSnippetContext.tsx +54 -18
- package/src/core/hooks/use-codeblock-tabs-controls.ts +2 -2
- package/src/core/hooks/use-local-state.ts +28 -19
- package/src/core/hooks/use-page-actions.ts +63 -6
- package/src/core/openapi/index.ts +1 -0
- package/src/core/types/l10n.ts +13 -0
- package/src/core/types/open-api-server.ts +1 -0
- package/src/icons/CursorIcon/CursorIcon.tsx +35 -0
- package/src/layouts/DocumentationLayout.tsx +3 -10
- package/src/markdoc/components/CodeGroup/CodeGroup.tsx +81 -52
- package/src/markdoc/components/Tabs/TabList.tsx +1 -0
|
@@ -12,11 +12,11 @@ export type PanelType = 'request' | 'responses' | 'request-samples' | 'response-
|
|
|
12
12
|
|
|
13
13
|
export type JsonProps = {
|
|
14
14
|
title?: CodeBlockControlsProps['title'];
|
|
15
|
+
controls?: CodeBlockControlsProps['controls'];
|
|
15
16
|
data: any;
|
|
16
17
|
className?: string;
|
|
17
18
|
expandLevel: number;
|
|
18
19
|
startLineNumber?: number;
|
|
19
|
-
hideHeader?: boolean;
|
|
20
20
|
onCopyClick?: () => void;
|
|
21
21
|
onPanelToggle?: (isExpanded: boolean, panelType?: PanelType) => void;
|
|
22
22
|
};
|
|
@@ -28,7 +28,7 @@ function JsonComponent({
|
|
|
28
28
|
onCopyClick,
|
|
29
29
|
onPanelToggle,
|
|
30
30
|
title,
|
|
31
|
-
|
|
31
|
+
controls = {},
|
|
32
32
|
}: JsonProps): JSX.Element {
|
|
33
33
|
const showFoldingButtons =
|
|
34
34
|
data && Object.values(data).some((value) => typeof value === 'object' && value !== null);
|
|
@@ -53,6 +53,8 @@ function JsonComponent({
|
|
|
53
53
|
|
|
54
54
|
const source = JSON.stringify(data, null, 2);
|
|
55
55
|
|
|
56
|
+
const hasHeader = title || controls;
|
|
57
|
+
|
|
56
58
|
return (
|
|
57
59
|
<JsonViewerWrap
|
|
58
60
|
data-testid="json-viewer"
|
|
@@ -61,17 +63,22 @@ function JsonComponent({
|
|
|
61
63
|
>
|
|
62
64
|
<CodeBlock
|
|
63
65
|
header={
|
|
64
|
-
|
|
65
|
-
?
|
|
66
|
-
: {
|
|
66
|
+
hasHeader
|
|
67
|
+
? {
|
|
67
68
|
title,
|
|
68
69
|
className: 'code-block-header',
|
|
69
|
-
controls: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
controls: controls && {
|
|
71
|
+
...controls,
|
|
72
|
+
copy: { ...controls.copy, data, onClick: onCopyClick, handleOutside: true },
|
|
73
|
+
expand: showFoldingButtons
|
|
74
|
+
? { ...controls.expand, onClick: expandAll }
|
|
75
|
+
: undefined,
|
|
76
|
+
collapse: showFoldingButtons
|
|
77
|
+
? { ...controls.collapse, onClick: collapseAll }
|
|
78
|
+
: undefined,
|
|
73
79
|
},
|
|
74
80
|
}
|
|
81
|
+
: undefined
|
|
75
82
|
}
|
|
76
83
|
source={source}
|
|
77
84
|
>
|
|
@@ -13,20 +13,22 @@ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
|
|
|
13
13
|
import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
|
|
14
14
|
import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
|
|
15
15
|
import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
|
|
16
|
-
import { usePageActions } from '@redocly/theme/core/hooks';
|
|
16
|
+
import { PageActionType, usePageActions } from '@redocly/theme/core/hooks';
|
|
17
17
|
|
|
18
18
|
type ActionState = 'idle' | 'processing' | 'done';
|
|
19
19
|
|
|
20
20
|
type PageActionProps = {
|
|
21
|
-
pageSlug
|
|
21
|
+
pageSlug?: string;
|
|
22
|
+
mcpUrl?: string;
|
|
23
|
+
actions?: PageActionType[];
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
const ACTION_DONE_DISPLAY_DURATION = 1000;
|
|
25
27
|
|
|
26
28
|
export function PageActions(props: PageActionProps): JSX.Element | null {
|
|
27
|
-
const { pageSlug } = props;
|
|
29
|
+
const { pageSlug, mcpUrl } = props;
|
|
28
30
|
|
|
29
|
-
const actions = usePageActions(pageSlug || '/');
|
|
31
|
+
const actions = usePageActions(pageSlug || '/', mcpUrl, props.actions);
|
|
30
32
|
|
|
31
33
|
const [actionState, setActionState] = useState<ActionState>('idle');
|
|
32
34
|
|
|
@@ -38,6 +38,8 @@ export const reverseRelationMap: Record<EntityRelationType, EntityRelationType>
|
|
|
38
38
|
memberOf: 'hasMember',
|
|
39
39
|
triggers: 'triggeredBy',
|
|
40
40
|
triggeredBy: 'triggers',
|
|
41
|
+
returns: 'returnedBy',
|
|
42
|
+
returnedBy: 'returns',
|
|
41
43
|
} as const;
|
|
42
44
|
|
|
43
45
|
export const relationTypeMap: Record<EntityRelationType, string> = {
|
|
@@ -66,6 +68,8 @@ export const relationTypeMap: Record<EntityRelationType, string> = {
|
|
|
66
68
|
memberOf: 'Member of',
|
|
67
69
|
triggers: 'Triggers',
|
|
68
70
|
triggeredBy: 'Triggered by',
|
|
71
|
+
returns: 'Returns',
|
|
72
|
+
returnedBy: 'Returned by',
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
export enum GraphHandleType {
|
|
@@ -1,31 +1,67 @@
|
|
|
1
|
-
import { createContext, useContext,
|
|
1
|
+
import React, { createContext, useContext, useCallback, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
activeSnippetName: string;
|
|
5
|
-
setActiveSnippetName: (name: string) => void;
|
|
6
|
-
};
|
|
3
|
+
import { useLocalState } from '../hooks/use-local-state';
|
|
7
4
|
|
|
8
5
|
export const CODE_GROUP_SNIPPET_NAME_KEY = 'redocly:codeGroupSnippetName';
|
|
9
6
|
|
|
7
|
+
type CodeSnippetContextType = {
|
|
8
|
+
activeSnippets: Record<string, string>;
|
|
9
|
+
setActiveSnippet: (groupId: string, snippetId: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
10
12
|
export const CodeSnippetContext = createContext<CodeSnippetContextType | null>(null);
|
|
11
13
|
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
export function CodeSnippetProvider({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const [activeSnippets, setActiveSnippets] = useLocalState<Record<string, string>>(
|
|
16
|
+
CODE_GROUP_SNIPPET_NAME_KEY,
|
|
17
|
+
{},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const setActiveSnippet = useCallback(
|
|
21
|
+
(groupId: string, snippetId: string) => {
|
|
22
|
+
setActiveSnippets({ ...activeSnippets, [groupId]: snippetId });
|
|
23
|
+
},
|
|
24
|
+
[activeSnippets, setActiveSnippets],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const contextValue = { activeSnippets, setActiveSnippet };
|
|
28
|
+
|
|
29
|
+
return <CodeSnippetContext.Provider value={contextValue}>{children}</CodeSnippetContext.Provider>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const useCodeSnippetContext = (): CodeSnippetContextType => {
|
|
15
33
|
const context = useContext(CodeSnippetContext);
|
|
34
|
+
|
|
16
35
|
if (!context) {
|
|
17
36
|
throw new Error('useCodeSnippetContext must be used within a CodeSnippetContext');
|
|
18
37
|
}
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
return context;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const useActiveCodeSnippetId = (
|
|
43
|
+
groupId?: string,
|
|
44
|
+
availableSnippets?: { id: string }[],
|
|
45
|
+
): [string, (id: string) => void] => {
|
|
46
|
+
const { activeSnippets, setActiveSnippet } = useCodeSnippetContext();
|
|
47
|
+
|
|
48
|
+
const storedSnippetId = groupId ? activeSnippets[groupId] || '' : '';
|
|
49
|
+
|
|
50
|
+
const activeId = useMemo(() => {
|
|
51
|
+
if (!availableSnippets?.length) return storedSnippetId;
|
|
52
|
+
|
|
53
|
+
const found = storedSnippetId && availableSnippets.find((s) => s.id === storedSnippetId);
|
|
54
|
+
return found ? storedSnippetId : availableSnippets[0]?.id || '';
|
|
55
|
+
}, [storedSnippetId, availableSnippets]);
|
|
56
|
+
|
|
57
|
+
const setActiveSnippetId = useCallback(
|
|
58
|
+
(id: string) => {
|
|
59
|
+
if (groupId) {
|
|
60
|
+
setActiveSnippet(groupId, id);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[groupId, setActiveSnippet],
|
|
22
64
|
);
|
|
23
65
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return [activeSnippetName, setActiveSnippetName];
|
|
27
|
-
} else {
|
|
28
|
-
// use global synced state for dropdown mode
|
|
29
|
-
return [context.activeSnippetName, context.setActiveSnippetName];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
66
|
+
return [activeId, setActiveSnippetId];
|
|
67
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { CodeBlockItems } from '../../components/CodeBlock/CodeBlock';
|
|
4
4
|
|
|
5
5
|
type CodeBlockTabsProps = {
|
|
6
|
-
tabs:
|
|
6
|
+
tabs: CodeBlockItems;
|
|
7
7
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
8
8
|
tabRefs: React.RefObject<HTMLButtonElement[]>;
|
|
9
9
|
};
|
|
@@ -1,30 +1,39 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { isBrowser } from '../utils/js-utils';
|
|
4
|
+
|
|
5
|
+
function getStoredValue<T>(key: string, fallback: T): T {
|
|
6
|
+
if (!isBrowser()) return fallback;
|
|
2
7
|
|
|
3
|
-
function getInitialValue<T>(key: string, initialValue: T): T {
|
|
4
|
-
if (typeof window === 'undefined') {
|
|
5
|
-
return initialValue;
|
|
6
|
-
}
|
|
7
8
|
try {
|
|
8
9
|
const savedValue = localStorage.getItem(key);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} catch (error) {
|
|
13
|
-
console.error(`Error reading from localStorage for key "${key}":`, error);
|
|
10
|
+
return savedValue ? (JSON.parse(savedValue) as T) : fallback;
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
14
13
|
}
|
|
15
|
-
return initialValue;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export function useLocalState<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
19
|
-
const [value, setValue] = useState<T>(
|
|
17
|
+
const [value, setValue] = useState<T>(initialValue);
|
|
20
18
|
|
|
19
|
+
// Load stored value from localStorage after component mounts
|
|
20
|
+
// This ensures SSR compatibility: server and client both start with initialValue,
|
|
21
|
+
// then client loads the actual stored value without hydration mismatch
|
|
21
22
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if (!isBrowser()) return;
|
|
24
|
+
setValue(getStoredValue(key, initialValue));
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
|
+
}, [key]);
|
|
27
|
+
|
|
28
|
+
const handleSetValue = useCallback(
|
|
29
|
+
(newValue: T) => {
|
|
30
|
+
setValue(newValue);
|
|
31
|
+
if (isBrowser()) {
|
|
32
|
+
localStorage.setItem(key, JSON.stringify(newValue));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[key],
|
|
36
|
+
);
|
|
28
37
|
|
|
29
|
-
return [value,
|
|
38
|
+
return [value, handleSetValue] as const;
|
|
30
39
|
}
|
|
@@ -12,10 +12,19 @@ import { useThemeHooks } from './use-theme-hooks';
|
|
|
12
12
|
import { useThemeConfig } from './use-theme-config';
|
|
13
13
|
import { ClipboardService } from '../utils/clipboard-service';
|
|
14
14
|
import { IS_BROWSER } from '../utils/dom';
|
|
15
|
+
import { CursorIcon } from '../../icons/CursorIcon/CursorIcon';
|
|
15
16
|
|
|
16
17
|
const DEFAULT_ENABLED_ACTIONS = ['copy', 'view', 'chatgpt', 'claude'] as const;
|
|
18
|
+
const CURSOR_URL =
|
|
19
|
+
'cursor://anysphere.cursor-deeplink/mcp/install?name=$NAME&config=$BASE64_ENCODED_CONFIG';
|
|
17
20
|
|
|
18
|
-
export
|
|
21
|
+
export type PageActionType = 'copy' | 'view' | 'chatgpt' | 'claude' | 'mcp-cursor';
|
|
22
|
+
|
|
23
|
+
export function usePageActions(
|
|
24
|
+
pageSlug: string,
|
|
25
|
+
mcpUrl?: string,
|
|
26
|
+
actions?: PageActionType[],
|
|
27
|
+
): PageAction[] {
|
|
19
28
|
const { useTranslate, usePageData, usePageProps, usePageSharedData } = useThemeHooks();
|
|
20
29
|
const { translate } = useTranslate();
|
|
21
30
|
|
|
@@ -32,7 +41,7 @@ export function usePageActions(pageSlug: string): PageAction[] {
|
|
|
32
41
|
);
|
|
33
42
|
const { isPublic } = usePageData() || {};
|
|
34
43
|
|
|
35
|
-
const
|
|
44
|
+
const result: PageAction[] = useMemo(() => {
|
|
36
45
|
if (shouldHideAllActions) {
|
|
37
46
|
return [];
|
|
38
47
|
}
|
|
@@ -54,9 +63,49 @@ export function usePageActions(pageSlug: string): PageAction[] {
|
|
|
54
63
|
return url.toString();
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
return (themeConfig.navigation?.actions?.items || DEFAULT_ENABLED_ACTIONS)
|
|
66
|
+
return (themeConfig.navigation?.actions?.items || actions || DEFAULT_ENABLED_ACTIONS)
|
|
58
67
|
.map((action) => {
|
|
68
|
+
function generateMCPConfig(isCursor?: boolean): string {
|
|
69
|
+
const jsonConfig = {
|
|
70
|
+
'mcp-server': {
|
|
71
|
+
url: mcpUrl,
|
|
72
|
+
description: 'MCP Server',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
if (isCursor) {
|
|
76
|
+
const url = CURSOR_URL.replace('$NAME', 'mcp-server').replace(
|
|
77
|
+
'$BASE64_ENCODED_CONFIG',
|
|
78
|
+
btoa(JSON.stringify(jsonConfig['mcp-server'])),
|
|
79
|
+
);
|
|
80
|
+
return url;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return JSON.stringify(jsonConfig, null, 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
switch (action) {
|
|
87
|
+
case 'mcp-cursor':
|
|
88
|
+
if (!mcpUrl) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
buttonText: translate('page.actions.cursorMcpButtonText', 'Connect to Cursor'),
|
|
94
|
+
title: translate('page.actions.cursorMcpTitle', 'Connect to Cursor'),
|
|
95
|
+
description: translate(
|
|
96
|
+
'page.actions.cursorMcpDescription',
|
|
97
|
+
'Install MCP server on Cursor',
|
|
98
|
+
),
|
|
99
|
+
iconComponent: CursorIcon,
|
|
100
|
+
onClick: () => {
|
|
101
|
+
try {
|
|
102
|
+
const url = generateMCPConfig(true);
|
|
103
|
+
window.open(url, '_blank');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(error);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
60
109
|
case 'copy':
|
|
61
110
|
return {
|
|
62
111
|
buttonText: translate('page.actions.copyButtonText', 'Copy'),
|
|
@@ -121,9 +170,17 @@ export function usePageActions(pageSlug: string): PageAction[] {
|
|
|
121
170
|
}
|
|
122
171
|
})
|
|
123
172
|
.filter((action) => action !== null);
|
|
124
|
-
}, [
|
|
125
|
-
|
|
126
|
-
|
|
173
|
+
}, [
|
|
174
|
+
shouldHideAllActions,
|
|
175
|
+
pageSlug,
|
|
176
|
+
themeConfig.navigation?.actions?.items,
|
|
177
|
+
actions,
|
|
178
|
+
mcpUrl,
|
|
179
|
+
translate,
|
|
180
|
+
isPublic,
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
return result;
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
function shouldHidePageActions(
|
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
addTrailingSlash,
|
|
18
18
|
withPathPrefix,
|
|
19
19
|
} from '../utils/urls';
|
|
20
|
+
export { capitalize } from '../utils/string';
|
|
20
21
|
export { typedMemo } from '../hoc/typedMemo';
|
|
21
22
|
export { useMount } from '../hooks/use-mount';
|
|
22
23
|
export { GlobalStyle } from '../styles/global';
|
package/src/core/types/l10n.ts
CHANGED
|
@@ -206,6 +206,9 @@ export type TranslationKey =
|
|
|
206
206
|
| 'page.actions.claudeTitle'
|
|
207
207
|
| 'page.actions.claudeButtonText'
|
|
208
208
|
| 'page.actions.claudeDescription'
|
|
209
|
+
| 'page.actions.cursorMcpButtonText'
|
|
210
|
+
| 'page.actions.cursorMcpTitle'
|
|
211
|
+
| 'page.actions.cursorMcpDescription'
|
|
209
212
|
| 'openapi.download.description.title'
|
|
210
213
|
| 'openapi.info.title'
|
|
211
214
|
| 'openapi.info.contact.url'
|
|
@@ -283,6 +286,16 @@ export type TranslationKey =
|
|
|
283
286
|
| 'openapi.schemaCatalogLink.title'
|
|
284
287
|
| 'openapi.schemaCatalogLink.copyButtonTooltip'
|
|
285
288
|
| 'openapi.schemaCatalogLink.copiedTooltip'
|
|
289
|
+
| 'openapi.mcp.title'
|
|
290
|
+
| 'openapi.mcp.endpoint'
|
|
291
|
+
| 'openapi.mcp.tools'
|
|
292
|
+
| 'openapi.mcp.protocolVersion'
|
|
293
|
+
| 'openapi.mcp.capabilities'
|
|
294
|
+
| 'openapi.mcp.experimentalCapabilities'
|
|
295
|
+
| 'openapi.mcp.inputSchema'
|
|
296
|
+
| 'openapi.mcp.inputExample'
|
|
297
|
+
| 'openapi.mcp.outputSchema'
|
|
298
|
+
| 'openapi.mcp.outputExample'
|
|
286
299
|
| 'asyncapi.download.description.title'
|
|
287
300
|
| 'asyncapi.info.title'
|
|
288
301
|
| 'graphql.queries'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { IconProps } from '@redocly/theme/icons/types';
|
|
5
|
+
|
|
6
|
+
const Icon = (props: IconProps) => (
|
|
7
|
+
<svg
|
|
8
|
+
width="16"
|
|
9
|
+
height="16"
|
|
10
|
+
viewBox="0 0 16 16"
|
|
11
|
+
fill="none"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path d="M7.99956 15V8.0001L2 11.4999L7.99956 15Z" fill="#939393" />
|
|
16
|
+
<path d="M14 4.49979L7.99956 15V8.0001L14 4.49979Z" fill="#E3E3E3" />
|
|
17
|
+
<path d="M2 4.49979H14L7.99956 8.0001L2 4.49979Z" fill="white" />
|
|
18
|
+
<path d="M8.00025 1V4.49995L14 4.49979L8.00025 1Z" fill="#444444" />
|
|
19
|
+
<path
|
|
20
|
+
d="M2 4.49979L8.00025 4.49995V1L2 4.49979ZM13.9999 11.4998L10.9999 9.74987L7.99956 15L13.9999 11.4998Z"
|
|
21
|
+
fill="#939393"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="M14 4.49979L10.9999 9.74987L13.9999 11.4998L14 4.49979ZM7.99956 8.0001L2 11.4999V4.49979L7.99956 8.0001Z"
|
|
25
|
+
fill="#444444"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const CursorIcon = styled(Icon).attrs(() => ({
|
|
31
|
+
'data-component-name': 'icons/CursorIcon/CursorIcon',
|
|
32
|
+
}))<IconProps>`
|
|
33
|
+
height: ${({ size }) => size || '16px'};
|
|
34
|
+
width: ${({ size }) => size || '16px'};
|
|
35
|
+
`;
|
|
@@ -9,12 +9,7 @@ import { breakpoints } from '@redocly/theme/core/utils';
|
|
|
9
9
|
import { PageNavigation } from '@redocly/theme/components/PageNavigation/PageNavigation';
|
|
10
10
|
import { LastUpdated } from '@redocly/theme/components/LastUpdated/LastUpdated';
|
|
11
11
|
import { Breadcrumbs as ThemeBreadcrumbs } from '@redocly/theme/components/Breadcrumbs/Breadcrumbs';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
CodeSnippetContext,
|
|
15
|
-
CODE_GROUP_SNIPPET_NAME_KEY,
|
|
16
|
-
} from '../core/contexts/CodeSnippetContext';
|
|
17
|
-
import { useLocalState } from '../core/hooks/use-local-state';
|
|
12
|
+
import { CodeSnippetProvider } from '@redocly/theme/core/contexts/CodeSnippetContext';
|
|
18
13
|
|
|
19
14
|
type DocumentationLayoutProps = {
|
|
20
15
|
tableOfContent: React.ReactNode;
|
|
@@ -44,10 +39,8 @@ export function DocumentationLayout({
|
|
|
44
39
|
const { editPage: themeEditPage } = config || {};
|
|
45
40
|
const mergedConf = editPage ? { ...themeEditPage, ...editPage } : undefined;
|
|
46
41
|
|
|
47
|
-
const [activeSnippetName, setActiveSnippetName] = useLocalState(CODE_GROUP_SNIPPET_NAME_KEY, '');
|
|
48
|
-
|
|
49
42
|
return (
|
|
50
|
-
<
|
|
43
|
+
<CodeSnippetProvider>
|
|
51
44
|
<LayoutWrapper data-component-name="Layout/DocumentationLayout" className={className}>
|
|
52
45
|
<ContentWrapper withToc={!config?.toc?.hide}>
|
|
53
46
|
<Breadcrumbs />
|
|
@@ -61,7 +54,7 @@ export function DocumentationLayout({
|
|
|
61
54
|
</ContentWrapper>
|
|
62
55
|
{tableOfContent}
|
|
63
56
|
</LayoutWrapper>
|
|
64
|
-
</
|
|
57
|
+
</CodeSnippetProvider>
|
|
65
58
|
);
|
|
66
59
|
}
|
|
67
60
|
|
|
@@ -5,65 +5,55 @@ import {
|
|
|
5
5
|
type CodeBlockProps,
|
|
6
6
|
} from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
7
7
|
import { langToName } from '@redocly/theme/core/utils';
|
|
8
|
-
import {
|
|
8
|
+
import { useActiveCodeSnippetId } from '@redocly/theme/core/contexts';
|
|
9
|
+
|
|
10
|
+
type SnippetData = {
|
|
11
|
+
name: string;
|
|
12
|
+
languageName: string;
|
|
13
|
+
lang: string;
|
|
14
|
+
props: CodeBlockProps;
|
|
15
|
+
id: string;
|
|
16
|
+
};
|
|
9
17
|
|
|
10
18
|
export function CodeGroup(props: React.PropsWithChildren<{ mode?: 'tabs' | 'dropdown' }>) {
|
|
11
19
|
const mode = props.mode || 'tabs';
|
|
12
20
|
const isTabsMode = mode === 'tabs';
|
|
13
21
|
|
|
14
22
|
const rawSnippets = React.useMemo(
|
|
15
|
-
() =>
|
|
16
|
-
React.Children.toArray(props.children).map((child, idx) => {
|
|
17
|
-
const childProps = child as React.ReactElement<CodeBlockProps>;
|
|
18
|
-
return {
|
|
19
|
-
name: getTabName(childProps.props, idx),
|
|
20
|
-
languageName: langToName(childProps.props.lang || 'Default'),
|
|
21
|
-
lang: childProps.props.lang || '',
|
|
22
|
-
props: childProps.props,
|
|
23
|
-
};
|
|
24
|
-
}),
|
|
23
|
+
() => parseSnippetsFromChildren(props.children),
|
|
25
24
|
[props.children],
|
|
26
25
|
);
|
|
27
26
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const getItemName = (snippet: (typeof rawSnippets)[number]) =>
|
|
35
|
-
isTabsMode ? snippet?.name : snippet?.languageName || '';
|
|
36
|
-
|
|
37
|
-
const name = getItemName(snippet);
|
|
38
|
-
|
|
39
|
-
const items = rawSnippets.map((item) => ({
|
|
40
|
-
name: getItemName(item),
|
|
41
|
-
lang: item.lang,
|
|
42
|
-
}));
|
|
43
|
-
const itemsProps = {
|
|
44
|
-
items,
|
|
45
|
-
onChange: (name: string | string[]) => {
|
|
46
|
-
setActiveSnippetName(name as string);
|
|
47
|
-
},
|
|
48
|
-
value: activeSnippetName || getItemName(rawSnippets[0]),
|
|
49
|
-
};
|
|
50
|
-
const snippetProps = {
|
|
51
|
-
...snippet.props,
|
|
52
|
-
header: {
|
|
53
|
-
...snippet.props.header,
|
|
54
|
-
title: isTabsMode ? undefined : snippet.name,
|
|
55
|
-
},
|
|
56
|
-
...(isTabsMode ? { tabs: itemsProps } : { dropdown: itemsProps }),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
return [name, snippetProps];
|
|
60
|
-
}),
|
|
61
|
-
),
|
|
62
|
-
[rawSnippets, activeSnippetName, isTabsMode, setActiveSnippetName],
|
|
63
|
-
);
|
|
27
|
+
const groupId = React.useMemo(() => generateGroupId(rawSnippets, mode), [rawSnippets, mode]);
|
|
28
|
+
|
|
29
|
+
const [activeSnippetId, setActiveSnippetId] = useActiveCodeSnippetId(groupId, rawSnippets);
|
|
30
|
+
|
|
31
|
+
const snippets = React.useMemo(() => {
|
|
32
|
+
const items = createItemsFromSnippets(rawSnippets, isTabsMode);
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
|
|
34
|
+
const itemsProps = {
|
|
35
|
+
items,
|
|
36
|
+
onChange: (id: string | string[]) => setActiveSnippetId(id as string),
|
|
37
|
+
value: activeSnippetId,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return Object.fromEntries(
|
|
41
|
+
rawSnippets.map((snippet: SnippetData) => {
|
|
42
|
+
const snippetProps = {
|
|
43
|
+
...snippet.props,
|
|
44
|
+
header: {
|
|
45
|
+
...snippet.props.header,
|
|
46
|
+
title: isTabsMode ? undefined : snippet.name,
|
|
47
|
+
},
|
|
48
|
+
...(isTabsMode ? { tabs: itemsProps } : { dropdown: itemsProps }),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return [snippet.id, snippetProps];
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}, [rawSnippets, activeSnippetId, isTabsMode, setActiveSnippetId]);
|
|
55
|
+
|
|
56
|
+
const activeSnippet = snippets[activeSnippetId];
|
|
67
57
|
if (!activeSnippet) {
|
|
68
58
|
return null;
|
|
69
59
|
}
|
|
@@ -71,8 +61,47 @@ export function CodeGroup(props: React.PropsWithChildren<{ mode?: 'tabs' | 'drop
|
|
|
71
61
|
return <CodeBlockComponent {...activeSnippet} />;
|
|
72
62
|
}
|
|
73
63
|
|
|
64
|
+
function generateContentHash(content: string): number {
|
|
65
|
+
let hash = 0;
|
|
66
|
+
for (let i = 0; i < content.length; i++) {
|
|
67
|
+
hash = content.charCodeAt(i) + ((hash << 5) - hash);
|
|
68
|
+
}
|
|
69
|
+
return Math.abs(hash);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate unique group ID for CodeGroup instance
|
|
73
|
+
// Examples: "dropdown-8901234", "tabs-1234567"
|
|
74
|
+
function generateGroupId(rawSnippets: SnippetData[], mode: string): string {
|
|
75
|
+
const content = rawSnippets.map((s) => s.id + (s.props.source || '')).join('|') + `|${mode}`;
|
|
76
|
+
const hash = generateContentHash(content);
|
|
77
|
+
|
|
78
|
+
return `${mode}-${hash}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
function getTabName(props: CodeBlockProps, idx: number): string {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const fallbackName = `Tab ${idx + 1}`;
|
|
83
|
+
return String(props.header?.title || props.file || langToName(props.lang || '') || fallbackName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseSnippetsFromChildren(children: React.ReactNode): SnippetData[] {
|
|
87
|
+
return React.Children.toArray(children).map((child, idx) => {
|
|
88
|
+
const childProps = child as React.ReactElement<CodeBlockProps>;
|
|
89
|
+
const props = childProps.props;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
name: getTabName(props, idx),
|
|
93
|
+
languageName: String(langToName(props.lang || 'Default') || ''),
|
|
94
|
+
lang: props.lang || '',
|
|
95
|
+
props,
|
|
96
|
+
id: `${props.lang || ''}-${idx}`,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createItemsFromSnippets(snippets: SnippetData[], isTabsMode: boolean) {
|
|
102
|
+
return snippets.map((snippet) => ({
|
|
103
|
+
name: isTabsMode ? snippet.name : snippet.languageName || '',
|
|
104
|
+
lang: snippet.lang,
|
|
105
|
+
id: snippet.id,
|
|
106
|
+
}));
|
|
78
107
|
}
|