@redocly/theme 0.58.0-next.8 → 0.58.0
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/CatalogClassic/CatalogClassic.js +9 -2
- 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/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/CatalogClassic/CatalogClassic.tsx +26 -10
- 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/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
|
@@ -2,10 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX } from 'react';
|
|
5
|
-
import type {
|
|
6
|
-
CodeBlockDropdownItems,
|
|
7
|
-
CodeBlockTabItems,
|
|
8
|
-
} from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
5
|
+
import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
9
6
|
|
|
10
7
|
import { CodeBlockTabs } from '@redocly/theme/components/CodeBlock/CodeBlockTabs';
|
|
11
8
|
import { CopyButton } from '@redocly/theme/components/Buttons/CopyButton';
|
|
@@ -24,8 +21,8 @@ export type CodeBlockControlsProps = {
|
|
|
24
21
|
className?: string;
|
|
25
22
|
title?: React.ReactNode | string;
|
|
26
23
|
controls?: ControlItems | false;
|
|
27
|
-
tabs?:
|
|
28
|
-
dropdown?:
|
|
24
|
+
tabs?: CodeBlockItems;
|
|
25
|
+
dropdown?: CodeBlockItems;
|
|
29
26
|
};
|
|
30
27
|
|
|
31
28
|
type ControlItems = {
|
|
@@ -87,7 +84,7 @@ export function CodeBlockControls({
|
|
|
87
84
|
{tabs && <CodeBlockTabs tabs={tabs} />}
|
|
88
85
|
<ControlsWrapper>
|
|
89
86
|
{dropdown && <CodeBlockDropdown {...dropdown} />}
|
|
90
|
-
{report && !report?.props?.hide ? (
|
|
87
|
+
{report && !report.hidden && !report?.props?.hide ? (
|
|
91
88
|
<TooltipWrapper
|
|
92
89
|
tip={translate('codeSnippet.report.tooltipText', 'Report a problem')}
|
|
93
90
|
placement="top"
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import React, { type JSX } from 'react';
|
|
2
|
-
import styled from 'styled-components';
|
|
3
2
|
|
|
4
|
-
import type {
|
|
3
|
+
import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
5
4
|
|
|
6
5
|
import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
|
|
7
6
|
import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
|
|
8
7
|
import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
|
|
9
8
|
import { Button } from '@redocly/theme/components/Button/Button';
|
|
10
|
-
import { ChevronSortIcon } from '@redocly/theme/icons/ChevronSortIcon/ChevronSortIcon';
|
|
11
9
|
import { NoneIcon } from '@redocly/theme/icons/NoneIcon/NoneIcon';
|
|
12
10
|
import { getFileIconByLanguage } from '@redocly/theme/core/utils';
|
|
13
11
|
|
|
14
|
-
export function CodeBlockDropdown({ items, onChange, value }:
|
|
15
|
-
const activeItem = items.find((item) => item.
|
|
12
|
+
export function CodeBlockDropdown({ items, onChange, value }: CodeBlockItems): JSX.Element {
|
|
13
|
+
const activeItem = items.find((item) => item.id === value) || items[0];
|
|
16
14
|
const icon = activeItem?.lang ? getFileIconByLanguage(activeItem?.lang) : null;
|
|
17
15
|
return (
|
|
18
|
-
<
|
|
16
|
+
<Dropdown
|
|
17
|
+
withArrow
|
|
19
18
|
alignment="end"
|
|
20
19
|
trigger={
|
|
21
|
-
<Button
|
|
20
|
+
<Button iconPosition="right" variant="ghost" size="small">
|
|
22
21
|
{icon}
|
|
23
22
|
{activeItem.name}
|
|
24
23
|
</Button>
|
|
@@ -27,11 +26,12 @@ export function CodeBlockDropdown({ items, onChange, value }: CodeBlockDropdownI
|
|
|
27
26
|
<DropdownMenu>
|
|
28
27
|
{items.map((item) => {
|
|
29
28
|
const icon = getFileIconByLanguage(item.lang || '');
|
|
29
|
+
const isActive = item.id === value;
|
|
30
30
|
return (
|
|
31
31
|
<DropdownMenuItem
|
|
32
|
-
key={item.
|
|
33
|
-
onAction={() => onChange(item.
|
|
34
|
-
active={
|
|
32
|
+
key={item.id}
|
|
33
|
+
onAction={() => onChange(item.id)}
|
|
34
|
+
active={isActive}
|
|
35
35
|
prefix={item.lang ? icon : <NoneIcon size="var(--icon-size)" />}
|
|
36
36
|
>
|
|
37
37
|
{item.name}
|
|
@@ -39,15 +39,6 @@ export function CodeBlockDropdown({ items, onChange, value }: CodeBlockDropdownI
|
|
|
39
39
|
);
|
|
40
40
|
})}
|
|
41
41
|
</DropdownMenu>
|
|
42
|
-
</
|
|
42
|
+
</Dropdown>
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
const StyledDropdown = styled(Dropdown)`
|
|
47
|
-
margin-left: auto;
|
|
48
|
-
--icon-size: 18px;
|
|
49
|
-
--button-color: var(--text-color-secondary);
|
|
50
|
-
button.button-size-small {
|
|
51
|
-
--button-icon-size: 18px;
|
|
52
|
-
}
|
|
53
|
-
`;
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
|
|
2
2
|
import styled, { css } from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX } from 'react';
|
|
5
|
-
import type {
|
|
5
|
+
import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
6
6
|
|
|
7
7
|
import { useCodeBlockTabsControls } from '@redocly/theme/core/hooks';
|
|
8
8
|
import { Button } from '@redocly/theme/components/Button/Button';
|
|
@@ -11,7 +11,7 @@ import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronR
|
|
|
11
11
|
import { getFileIconByExt, getFileIconByLanguage } from '@redocly/theme/core/utils/get-file-icon';
|
|
12
12
|
|
|
13
13
|
export type CodeBlockTabsProps = {
|
|
14
|
-
tabs:
|
|
14
|
+
tabs: CodeBlockItems;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
|
|
@@ -24,7 +24,7 @@ export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
useEffect(() => {
|
|
27
|
-
const activeTab = tabRefs.current.find((tab) => tab?.dataset.
|
|
27
|
+
const activeTab = tabRefs.current.find((tab) => tab?.dataset.id === tabs.value);
|
|
28
28
|
|
|
29
29
|
if (activeTab) {
|
|
30
30
|
activeTab.scrollIntoView({ block: 'nearest', inline: 'center' });
|
|
@@ -35,23 +35,23 @@ export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
|
|
|
35
35
|
<CodeBlockTabsWrapper ref={containerRef} data-component-name="CodeBlock/CodeBlockTabs">
|
|
36
36
|
<ShadowWrapper>
|
|
37
37
|
<Tabs>
|
|
38
|
-
{tabs.items.map((
|
|
38
|
+
{tabs.items.map((item, i) => {
|
|
39
|
+
const { name, lang, id } = item;
|
|
39
40
|
const ext = name.match(/\.([^.]+)$/)?.[1];
|
|
40
41
|
const fileIcon = lang
|
|
41
42
|
? getFileIconByLanguage(lang)
|
|
42
43
|
: ext
|
|
43
44
|
? getFileIconByExt(ext)
|
|
44
45
|
: null;
|
|
45
|
-
|
|
46
46
|
return (
|
|
47
47
|
<Tab
|
|
48
48
|
ref={(el: HTMLButtonElement | null) => {
|
|
49
49
|
tabRefs.current[i] = el as HTMLButtonElement;
|
|
50
50
|
}}
|
|
51
51
|
data-name={name}
|
|
52
|
-
active={
|
|
53
|
-
key={
|
|
54
|
-
onClick={() => tabs.onChange(
|
|
52
|
+
active={id === tabs.value}
|
|
53
|
+
key={id}
|
|
54
|
+
onClick={() => tabs.onChange(id)}
|
|
55
55
|
>
|
|
56
56
|
{fileIcon}
|
|
57
57
|
{name}
|
|
@@ -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
|
|