@redocly/theme 0.69.0-next.1 → 0.69.0-next.3
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/Badge/Badge.d.ts +3 -1
- package/lib/components/Badge/Badge.js +7 -2
- package/lib/components/Buttons/ConnectMCPButton.js +43 -14
- package/lib/components/MCP/McpConnectionTabs.d.ts +7 -0
- package/lib/components/MCP/McpConnectionTabs.js +151 -0
- package/lib/core/constants/mcp.d.ts +2 -0
- package/lib/core/constants/mcp.js +8 -1
- package/lib/core/hooks/use-active-heading.js +59 -15
- package/lib/core/hooks/use-active-section-id.js +3 -1
- package/lib/core/hooks/use-connect-mcp-button.d.ts +1 -1
- package/lib/core/hooks/use-connect-mcp-button.js +16 -13
- package/lib/core/styles/global.js +6 -1
- package/lib/core/types/catalog.d.ts +2 -0
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/mcp.d.ts +1 -1
- package/lib/core/utils/mcp.d.ts +5 -0
- package/lib/core/utils/mcp.js +35 -0
- package/lib/icons/CodexIcon/CodexIcon.d.ts +25 -0
- package/lib/icons/CodexIcon/CodexIcon.js +22 -0
- package/lib/layouts/McpNotAllowed.js +5 -3
- package/lib/markdoc/components/Cards/Card.d.ts +8 -1
- package/lib/markdoc/components/Cards/Card.js +55 -16
- package/lib/markdoc/components/Cards/CardBadge.d.ts +9 -0
- package/lib/markdoc/components/Cards/CardBadge.js +30 -0
- package/lib/markdoc/components/Cards/CardIcon.d.ts +5 -4
- package/lib/markdoc/components/Cards/CardIcon.js +6 -3
- package/lib/markdoc/components/Cards/variables.dark.js +32 -0
- package/lib/markdoc/components/Cards/variables.js +76 -4
- package/lib/markdoc/tags/card.js +14 -0
- package/lib/markdoc/tags/connect-mcp.js +2 -1
- package/package.json +2 -2
- package/src/components/Badge/Badge.tsx +16 -2
- package/src/components/Buttons/ConnectMCPButton.tsx +44 -14
- package/src/components/MCP/McpConnectionTabs.tsx +263 -0
- package/src/core/constants/mcp.ts +10 -0
- package/src/core/hooks/use-active-heading.ts +79 -15
- package/src/core/hooks/use-active-section-id.ts +3 -1
- package/src/core/hooks/use-connect-mcp-button.ts +23 -15
- package/src/core/styles/global.ts +6 -1
- package/src/core/types/catalog.ts +3 -0
- package/src/core/types/l10n.ts +17 -0
- package/src/core/types/mcp.ts +1 -1
- package/src/core/utils/mcp.ts +39 -0
- package/src/icons/CodexIcon/CodexIcon.tsx +23 -0
- package/src/layouts/McpNotAllowed.tsx +4 -2
- package/src/markdoc/components/Cards/Card.tsx +79 -14
- package/src/markdoc/components/Cards/CardBadge.tsx +39 -0
- package/src/markdoc/components/Cards/CardIcon.tsx +16 -5
- package/src/markdoc/components/Cards/variables.dark.ts +32 -0
- package/src/markdoc/components/Cards/variables.ts +76 -4
- package/src/markdoc/tags/card.ts +14 -0
- package/src/markdoc/tags/connect-mcp.ts +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.connectMcp = void 0;
|
|
4
|
+
const constants_1 = require("../../core/constants");
|
|
4
5
|
exports.connectMcp = {
|
|
5
6
|
schema: {
|
|
6
7
|
render: 'ConnectMCP',
|
|
@@ -17,7 +18,7 @@ exports.connectMcp = {
|
|
|
17
18
|
},
|
|
18
19
|
options: {
|
|
19
20
|
type: Array,
|
|
20
|
-
default:
|
|
21
|
+
default: constants_1.DEFAULT_MCP_OPTIONS,
|
|
21
22
|
},
|
|
22
23
|
},
|
|
23
24
|
selfClosing: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/theme",
|
|
3
|
-
"version": "0.69.0-next.
|
|
3
|
+
"version": "0.69.0-next.3",
|
|
4
4
|
"description": "Shared UI components lib",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"theme",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"vitest": "4.1.10",
|
|
69
69
|
"vitest-when": "0.6.2",
|
|
70
70
|
"webpack": "5.105.2",
|
|
71
|
-
"@redocly/realm-asyncapi-sdk": "0.15.0-next.
|
|
71
|
+
"@redocly/realm-asyncapi-sdk": "0.15.0-next.2"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@tanstack/react-query": "5.62.3",
|
|
@@ -11,9 +11,18 @@ export type BadgeProps = PropsWithChildren<{
|
|
|
11
11
|
key?: string;
|
|
12
12
|
className?: string;
|
|
13
13
|
icon?: string;
|
|
14
|
+
/** Inline SVG content for the icon */
|
|
15
|
+
iconRawContent?: string;
|
|
14
16
|
}>;
|
|
15
17
|
|
|
16
|
-
export function Badge({
|
|
18
|
+
export function Badge({
|
|
19
|
+
icon,
|
|
20
|
+
iconRawContent,
|
|
21
|
+
children,
|
|
22
|
+
deprecated,
|
|
23
|
+
color,
|
|
24
|
+
...props
|
|
25
|
+
}: BadgeProps): JSX.Element {
|
|
17
26
|
return (
|
|
18
27
|
<BadgeComponent
|
|
19
28
|
{...props}
|
|
@@ -21,7 +30,7 @@ export function Badge({ icon, children, deprecated, color, ...props }: BadgeProp
|
|
|
21
30
|
$deprecated={deprecated}
|
|
22
31
|
data-component-name="Badge/Badge"
|
|
23
32
|
>
|
|
24
|
-
{icon ? <BadgeIcon icon={icon} /> : null}
|
|
33
|
+
{icon || iconRawContent ? <BadgeIcon icon={icon} rawContent={iconRawContent} /> : null}
|
|
25
34
|
{children}
|
|
26
35
|
</BadgeComponent>
|
|
27
36
|
);
|
|
@@ -54,4 +63,9 @@ const BadgeIcon = styled(GenericIcon)`
|
|
|
54
63
|
margin-right: var(--spacing-xxs);
|
|
55
64
|
flex-shrink: 0;
|
|
56
65
|
vertical-align: middle;
|
|
66
|
+
|
|
67
|
+
/* For inline SVG to align properly */
|
|
68
|
+
svg {
|
|
69
|
+
display: block;
|
|
70
|
+
}
|
|
57
71
|
`;
|
|
@@ -9,11 +9,14 @@ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMen
|
|
|
9
9
|
import { Button } from '@redocly/theme/components/Button/Button';
|
|
10
10
|
import { CursorIcon } from '@redocly/theme/icons/CursorIcon/CursorIcon';
|
|
11
11
|
import { VSCodeIcon } from '@redocly/theme/icons/VSCodeIcon/VSCodeIcon';
|
|
12
|
+
import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
|
|
13
|
+
import { CodexIcon } from '@redocly/theme/icons/CodexIcon/CodexIcon';
|
|
12
14
|
import { CopyIcon } from '@redocly/theme/icons/CopyIcon/CopyIcon';
|
|
13
15
|
import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
|
|
14
16
|
import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
|
|
15
17
|
import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
|
|
16
18
|
import { useConnectMCPButton, useOutsideClick, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
19
|
+
import { DEFAULT_MCP_OPTIONS } from '@redocly/theme/core/constants';
|
|
17
20
|
|
|
18
21
|
type MenuOption = {
|
|
19
22
|
key: MCPOption;
|
|
@@ -41,6 +44,22 @@ const MENU_OPTIONS: MenuOption[] = [
|
|
|
41
44
|
descriptionTranslationKey: 'page.actions.connectMcp.vscodeDescription',
|
|
42
45
|
descriptionDefault: 'Install MCP server on VS Code',
|
|
43
46
|
},
|
|
47
|
+
{
|
|
48
|
+
key: 'claude-code',
|
|
49
|
+
icon: ClaudeIcon,
|
|
50
|
+
titleTranslationKey: 'page.actions.connectMcp.claudeCode',
|
|
51
|
+
titleDefault: 'Connect to Claude Code',
|
|
52
|
+
descriptionTranslationKey: 'page.actions.connectMcp.claudeCodeDescription',
|
|
53
|
+
descriptionDefault: 'Copy the Claude Code CLI command',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'codex',
|
|
57
|
+
icon: CodexIcon,
|
|
58
|
+
titleTranslationKey: 'page.actions.connectMcp.codex',
|
|
59
|
+
titleDefault: 'Connect to Codex CLI',
|
|
60
|
+
descriptionTranslationKey: 'page.actions.connectMcp.codexDescription',
|
|
61
|
+
descriptionDefault: 'Copy the Codex CLI command',
|
|
62
|
+
},
|
|
44
63
|
{
|
|
45
64
|
key: 'copy',
|
|
46
65
|
icon: CopyIcon,
|
|
@@ -51,6 +70,9 @@ const MENU_OPTIONS: MenuOption[] = [
|
|
|
51
70
|
},
|
|
52
71
|
];
|
|
53
72
|
|
|
73
|
+
// Copy actions keep the dropdown open so the copied checkmark is visible.
|
|
74
|
+
const COPY_OPTIONS: MCPOption[] = ['claude-code', 'codex', 'copy'];
|
|
75
|
+
|
|
54
76
|
export type ConnectMCPButtonProps = {
|
|
55
77
|
placement?: 'top' | 'bottom';
|
|
56
78
|
alignment?: 'start' | 'end';
|
|
@@ -60,14 +82,14 @@ export type ConnectMCPButtonProps = {
|
|
|
60
82
|
export function ConnectMCPButton({
|
|
61
83
|
placement = 'bottom',
|
|
62
84
|
alignment = 'end',
|
|
63
|
-
options =
|
|
85
|
+
options = DEFAULT_MCP_OPTIONS,
|
|
64
86
|
}: ConnectMCPButtonProps): JSX.Element | null {
|
|
65
87
|
const { useTranslate } = useThemeHooks();
|
|
66
88
|
const { translate } = useTranslate();
|
|
67
89
|
const [isOpen, setIsOpen] = useState(false);
|
|
68
90
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
69
91
|
|
|
70
|
-
const {
|
|
92
|
+
const { copiedOption, visibleOptions, handleAction } = useConnectMCPButton({ options });
|
|
71
93
|
|
|
72
94
|
useOutsideClick(wrapperRef, () => setIsOpen(false));
|
|
73
95
|
|
|
@@ -78,13 +100,13 @@ export function ConnectMCPButton({
|
|
|
78
100
|
if (!primaryOption) return null;
|
|
79
101
|
|
|
80
102
|
const PrimaryIcon = primaryOption.icon;
|
|
81
|
-
|
|
82
|
-
// primary button itself must reflect it.
|
|
83
|
-
const showPrimaryCopied = primaryOption.key === 'copy' && isCopied;
|
|
103
|
+
const showPrimaryCopied = copiedOption === primaryOption.key;
|
|
84
104
|
|
|
85
105
|
const handleMenuAction = (key: MCPOption) => {
|
|
86
106
|
handleAction(key);
|
|
87
|
-
|
|
107
|
+
if (!COPY_OPTIONS.includes(key)) {
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
}
|
|
88
110
|
};
|
|
89
111
|
|
|
90
112
|
return (
|
|
@@ -121,7 +143,7 @@ export function ConnectMCPButton({
|
|
|
121
143
|
<DropdownMenu>
|
|
122
144
|
{dropdownOptions.map((option) => {
|
|
123
145
|
const Icon = option.icon;
|
|
124
|
-
const showCheckmark = option.key
|
|
146
|
+
const showCheckmark = copiedOption === option.key;
|
|
125
147
|
|
|
126
148
|
return (
|
|
127
149
|
<DropdownMenuItem key={option.key} onAction={() => handleMenuAction(option.key)}>
|
|
@@ -162,17 +184,25 @@ const SplitButtonGroup = styled.div`
|
|
|
162
184
|
align-items: stretch;
|
|
163
185
|
`;
|
|
164
186
|
|
|
187
|
+
/* Doubled selectors outrank the base button class in any style injection order,
|
|
188
|
+
and margin: 0 keeps themed button margins from splitting the group apart. */
|
|
165
189
|
const PrimaryButton = styled(Button)`
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
&& {
|
|
191
|
+
--button-gap: var(--connect-mcp-button-gap);
|
|
192
|
+
margin: 0;
|
|
193
|
+
border-top-right-radius: 0;
|
|
194
|
+
border-bottom-right-radius: 0;
|
|
195
|
+
}
|
|
169
196
|
`;
|
|
170
197
|
|
|
171
198
|
const ChevronButton = styled(Button)`
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
&& {
|
|
200
|
+
margin: 0;
|
|
201
|
+
border-top-left-radius: 0;
|
|
202
|
+
border-bottom-left-radius: 0;
|
|
203
|
+
border-left: none;
|
|
204
|
+
padding-inline: var(--connect-mcp-button-chevron-padding-inline);
|
|
205
|
+
}
|
|
176
206
|
`;
|
|
177
207
|
|
|
178
208
|
const DropdownWrapper = styled.div<{ $placement?: string; $alignment?: string }>`
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX, ReactNode } from 'react';
|
|
5
|
+
import type { TFunction } from '@redocly/theme/core/types';
|
|
6
|
+
|
|
7
|
+
import { useMCPConfig, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
|
+
import {
|
|
9
|
+
generateClaudeCodeCommand,
|
|
10
|
+
generateClaudeDesktopConfig,
|
|
11
|
+
generateCodexCommand,
|
|
12
|
+
generateMcpInstallPrompt,
|
|
13
|
+
} from '@redocly/theme/core/utils';
|
|
14
|
+
import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
15
|
+
import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
|
|
16
|
+
import { ChatGptIcon } from '@redocly/theme/icons/ChatGptIcon/ChatGptIcon';
|
|
17
|
+
import { Tabs, TabsSize } from '@redocly/theme/markdoc/components/Tabs/Tabs';
|
|
18
|
+
import { TabItemFragment } from '@redocly/theme/markdoc/components/Tabs/TabItemFragment';
|
|
19
|
+
import { NumberedList } from '@redocly/theme/markdoc/components/NumberedList/NumberedList';
|
|
20
|
+
import { NumberedItem } from '@redocly/theme/markdoc/components/NumberedList/NumberedItem';
|
|
21
|
+
|
|
22
|
+
export type McpConnectionTab = 'claude' | 'chatgpt' | 'json';
|
|
23
|
+
|
|
24
|
+
export type McpConnectionTabsProps = {
|
|
25
|
+
options?: McpConnectionTab[];
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TAB_OPTIONS: McpConnectionTab[] = ['claude', 'chatgpt', 'json'];
|
|
30
|
+
|
|
31
|
+
const TAB_LABELS: Record<
|
|
32
|
+
McpConnectionTab,
|
|
33
|
+
{ translationKey: string; defaultLabel: string; icon?: React.ComponentType }
|
|
34
|
+
> = {
|
|
35
|
+
claude: {
|
|
36
|
+
translationKey: 'page.mcpNotAllowed.tabs.claude',
|
|
37
|
+
defaultLabel: 'Claude',
|
|
38
|
+
icon: ClaudeIcon,
|
|
39
|
+
},
|
|
40
|
+
chatgpt: {
|
|
41
|
+
translationKey: 'page.mcpNotAllowed.tabs.chatgpt',
|
|
42
|
+
defaultLabel: 'ChatGPT',
|
|
43
|
+
icon: ChatGptIcon,
|
|
44
|
+
},
|
|
45
|
+
json: { translationKey: 'page.mcpNotAllowed.tabs.json', defaultLabel: 'JSON' },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type TabClient = { headline: string; content: ReactNode };
|
|
49
|
+
|
|
50
|
+
// The report control is docs feedback, not useful on the connection page.
|
|
51
|
+
const CODE_BLOCK_HEADER = { controls: { copy: {}, report: { hidden: true } } };
|
|
52
|
+
|
|
53
|
+
export function McpConnectionTabs({
|
|
54
|
+
options = DEFAULT_TAB_OPTIONS,
|
|
55
|
+
className,
|
|
56
|
+
}: McpConnectionTabsProps): JSX.Element | null {
|
|
57
|
+
const { useTranslate } = useThemeHooks();
|
|
58
|
+
const { translate } = useTranslate();
|
|
59
|
+
const { serverName, serverUrl, isMcpDisabled } = useMCPConfig();
|
|
60
|
+
// The SSR markup of error pages is cached with the build-time origin, so the
|
|
61
|
+
// tabs remount after hydration to pick up the real browser origin.
|
|
62
|
+
const [hydrated, setHydrated] = useState(false);
|
|
63
|
+
|
|
64
|
+
useEffect(() => setHydrated(true), []);
|
|
65
|
+
|
|
66
|
+
if (isMcpDisabled) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const connection = { serverName, url: serverUrl };
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<McpConnectionTabsWrapper
|
|
74
|
+
key={hydrated ? 'client' : 'ssr'}
|
|
75
|
+
data-component-name="MCP/McpConnectionTabs"
|
|
76
|
+
data-testid="mcp-connection-tabs"
|
|
77
|
+
className={className}
|
|
78
|
+
>
|
|
79
|
+
<Tabs size={TabsSize.SMALL}>
|
|
80
|
+
{options.map((option) => {
|
|
81
|
+
const { translationKey, defaultLabel, icon: TabIcon } = TAB_LABELS[option];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<TabItemFragment
|
|
85
|
+
key={option}
|
|
86
|
+
label={translate(translationKey, defaultLabel)}
|
|
87
|
+
icon={TabIcon ? <TabIcon /> : undefined}
|
|
88
|
+
>
|
|
89
|
+
{renderTabContent(option, connection, translate)}
|
|
90
|
+
</TabItemFragment>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</Tabs>
|
|
94
|
+
</McpConnectionTabsWrapper>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderTabContent(
|
|
99
|
+
option: McpConnectionTab,
|
|
100
|
+
connection: { serverName: string; url: string },
|
|
101
|
+
translate: TFunction,
|
|
102
|
+
): ReactNode {
|
|
103
|
+
const terminalHint = (
|
|
104
|
+
<TabHint>
|
|
105
|
+
{translate('page.mcpNotAllowed.snippets.terminal.hint', 'Run this command in your terminal:')}
|
|
106
|
+
</TabHint>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const urlBlock = <CodeBlock lang="text" source={connection.url} header={CODE_BLOCK_HEADER} />;
|
|
110
|
+
|
|
111
|
+
const assistantPromptClient: TabClient = {
|
|
112
|
+
headline: translate('page.mcpNotAllowed.snippets.prompt.headline', 'Prompt'),
|
|
113
|
+
content: (
|
|
114
|
+
<>
|
|
115
|
+
<TabHint>
|
|
116
|
+
{translate(
|
|
117
|
+
'page.mcpNotAllowed.snippets.prompt.hint',
|
|
118
|
+
'Or paste this prompt into the chat to have it add the server for you:',
|
|
119
|
+
)}
|
|
120
|
+
</TabHint>
|
|
121
|
+
<CodeBlock
|
|
122
|
+
lang="text"
|
|
123
|
+
source={generateMcpInstallPrompt(connection)}
|
|
124
|
+
header={CODE_BLOCK_HEADER}
|
|
125
|
+
wrapContents
|
|
126
|
+
/>
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
switch (option) {
|
|
132
|
+
case 'claude':
|
|
133
|
+
return renderClientList([
|
|
134
|
+
{
|
|
135
|
+
headline: translate('page.mcpNotAllowed.snippets.claudeCode.headline', 'Claude Code'),
|
|
136
|
+
content: (
|
|
137
|
+
<>
|
|
138
|
+
{terminalHint}
|
|
139
|
+
<CodeBlock
|
|
140
|
+
lang="shell"
|
|
141
|
+
source={generateClaudeCodeCommand(connection)}
|
|
142
|
+
header={CODE_BLOCK_HEADER}
|
|
143
|
+
wrapContents
|
|
144
|
+
/>
|
|
145
|
+
</>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
headline: translate(
|
|
150
|
+
'page.mcpNotAllowed.snippets.claudeDesktop.headline',
|
|
151
|
+
'Claude Desktop',
|
|
152
|
+
),
|
|
153
|
+
content: (
|
|
154
|
+
<>
|
|
155
|
+
<TabHint>
|
|
156
|
+
{translate(
|
|
157
|
+
'page.mcpNotAllowed.snippets.claudeDesktop.instruction',
|
|
158
|
+
'In Claude Desktop, open Settings → Developer → Edit Config, add this to the configuration file, and restart Claude Desktop:',
|
|
159
|
+
)}
|
|
160
|
+
</TabHint>
|
|
161
|
+
<CodeBlock
|
|
162
|
+
lang="json"
|
|
163
|
+
source={generateClaudeDesktopConfig(connection)}
|
|
164
|
+
header={CODE_BLOCK_HEADER}
|
|
165
|
+
/>
|
|
166
|
+
</>
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
case 'chatgpt':
|
|
171
|
+
return renderClientList([
|
|
172
|
+
{
|
|
173
|
+
headline: translate('page.mcpNotAllowed.snippets.codexCli.headline', 'Codex CLI'),
|
|
174
|
+
content: (
|
|
175
|
+
<>
|
|
176
|
+
{terminalHint}
|
|
177
|
+
<CodeBlock
|
|
178
|
+
lang="shell"
|
|
179
|
+
source={generateCodexCommand(connection)}
|
|
180
|
+
header={CODE_BLOCK_HEADER}
|
|
181
|
+
wrapContents
|
|
182
|
+
/>
|
|
183
|
+
</>
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
headline: translate(
|
|
188
|
+
'page.mcpNotAllowed.snippets.chatgptApp.headline',
|
|
189
|
+
'ChatGPT desktop app',
|
|
190
|
+
),
|
|
191
|
+
content: (
|
|
192
|
+
<>
|
|
193
|
+
<TabHint>
|
|
194
|
+
{translate(
|
|
195
|
+
'page.mcpNotAllowed.snippets.chatgptApp.instruction',
|
|
196
|
+
'In the ChatGPT desktop app, go to Settings → Plugins → MCPs and add a server with the "Streamable HTTP" type and this URL (restart the app afterwards):',
|
|
197
|
+
)}
|
|
198
|
+
</TabHint>
|
|
199
|
+
{urlBlock}
|
|
200
|
+
</>
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
assistantPromptClient,
|
|
204
|
+
]);
|
|
205
|
+
case 'json':
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<TabHint>
|
|
209
|
+
{translate(
|
|
210
|
+
'page.mcpNotAllowed.snippets.json.hint',
|
|
211
|
+
'Add this server entry to your MCP client configuration file:',
|
|
212
|
+
)}
|
|
213
|
+
</TabHint>
|
|
214
|
+
<CodeBlock
|
|
215
|
+
lang="json"
|
|
216
|
+
source={JSON.stringify(
|
|
217
|
+
{ [connection.serverName]: { url: connection.url, description: 'MCP Server' } },
|
|
218
|
+
null,
|
|
219
|
+
2,
|
|
220
|
+
)}
|
|
221
|
+
header={CODE_BLOCK_HEADER}
|
|
222
|
+
/>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderClientList(clients: TabClient[]): ReactNode {
|
|
229
|
+
return (
|
|
230
|
+
<NumberedList>
|
|
231
|
+
{clients.map((client, index) => (
|
|
232
|
+
<NumberedItem
|
|
233
|
+
key={index}
|
|
234
|
+
type="dot"
|
|
235
|
+
size="small"
|
|
236
|
+
headline={client.headline}
|
|
237
|
+
index={index + 1}
|
|
238
|
+
isLast={index === clients.length - 1}
|
|
239
|
+
>
|
|
240
|
+
{client.content}
|
|
241
|
+
</NumberedItem>
|
|
242
|
+
))}
|
|
243
|
+
</NumberedList>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const McpConnectionTabsWrapper = styled.div`
|
|
248
|
+
--md-numbered-list-margin: 0 0 var(--spacing-sm);
|
|
249
|
+
|
|
250
|
+
font-size: var(--page-mcp-405-snippets-font-size);
|
|
251
|
+
line-height: var(--page-mcp-405-snippets-line-height);
|
|
252
|
+
text-align: left;
|
|
253
|
+
|
|
254
|
+
/* The copy control floats over the code, so wrapped lines need room to clear it. */
|
|
255
|
+
pre[data-testid='source-code'] {
|
|
256
|
+
padding-right: var(--page-mcp-405-snippets-code-padding-right);
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const TabHint = styled.p`
|
|
261
|
+
margin: 0 0 var(--spacing-xs);
|
|
262
|
+
color: var(--page-mcp-405-snippets-hint-text-color);
|
|
263
|
+
`;
|
|
@@ -8,6 +8,15 @@ type HeadingEntry = {
|
|
|
8
8
|
[key: string]: IntersectionObserverEntry;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
type ClickScroll = {
|
|
12
|
+
target: HTMLElement;
|
|
13
|
+
scrollMargin: number;
|
|
14
|
+
closestDistance: number;
|
|
15
|
+
reached: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const TARGET_TOLERANCE = 2;
|
|
19
|
+
|
|
11
20
|
export function useActiveHeading(
|
|
12
21
|
contentElement: HTMLDivElement | null,
|
|
13
22
|
displayedHeadings?: Array<{ id: string; depth: number } | null>,
|
|
@@ -17,11 +26,14 @@ export function useActiveHeading(
|
|
|
17
26
|
const headingElementsRef = useRef<HeadingEntry>({});
|
|
18
27
|
const displayedHeadingsRef = useRef(displayedHeadings);
|
|
19
28
|
const lockObserver = useRef<boolean>(false);
|
|
29
|
+
const clickScroll = useRef<ClickScroll | null>(null);
|
|
20
30
|
const location = useLocation();
|
|
21
31
|
|
|
22
32
|
useEffect(() => {
|
|
23
33
|
setHeading(undefined);
|
|
24
34
|
headingElementsRef.current = {};
|
|
35
|
+
clickScroll.current = null;
|
|
36
|
+
lockObserver.current = false;
|
|
25
37
|
}, [location.pathname]);
|
|
26
38
|
|
|
27
39
|
useEffect(() => {
|
|
@@ -133,10 +145,6 @@ export function useActiveHeading(
|
|
|
133
145
|
|
|
134
146
|
const intersectionCallback = useCallback(
|
|
135
147
|
(entries: IntersectionObserverEntry[]) => {
|
|
136
|
-
if (lockObserver.current) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
148
|
headingElementsRef.current = entries.reduce(
|
|
141
149
|
(map: HeadingEntry, entry: IntersectionObserverEntry) => {
|
|
142
150
|
map[entry.target.id] = entry;
|
|
@@ -145,6 +153,10 @@ export function useActiveHeading(
|
|
|
145
153
|
headingElementsRef.current,
|
|
146
154
|
);
|
|
147
155
|
|
|
156
|
+
if (lockObserver.current) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
if (isTopOfPage()) {
|
|
149
161
|
const newHeading = headingElements[0]?.id || undefined;
|
|
150
162
|
setHeading(newHeading);
|
|
@@ -202,22 +214,71 @@ export function useActiveHeading(
|
|
|
202
214
|
[getIndexFromId, headingElements, getTriggerOffset, findActiveHeaderByVirtualPosition],
|
|
203
215
|
);
|
|
204
216
|
|
|
217
|
+
const releaseClickScroll = useCallback(() => {
|
|
218
|
+
clickScroll.current = null;
|
|
219
|
+
lockObserver.current = false;
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const trackClickScroll = useCallback(() => {
|
|
223
|
+
const clickScrollState = clickScroll.current;
|
|
224
|
+
if (!clickScrollState) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { top } = clickScrollState.target.getBoundingClientRect();
|
|
229
|
+
|
|
230
|
+
if (clickScrollState.reached) {
|
|
231
|
+
if (top >= 0 && top <= getTriggerOffset()) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const distance = Math.abs(top - clickScrollState.scrollMargin);
|
|
236
|
+
|
|
237
|
+
if (distance <= TARGET_TOLERANCE) {
|
|
238
|
+
clickScrollState.reached = true;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (distance <= clickScrollState.closestDistance + TARGET_TOLERANCE) {
|
|
243
|
+
clickScrollState.closestDistance = Math.min(clickScrollState.closestDistance, distance);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
releaseClickScroll();
|
|
249
|
+
intersectionCallback([]);
|
|
250
|
+
}, [releaseClickScroll, intersectionCallback, getTriggerOffset]);
|
|
251
|
+
|
|
205
252
|
const handleHeadingClick = useCallback(
|
|
206
253
|
(headingId: string) => {
|
|
207
|
-
|
|
254
|
+
releaseClickScroll();
|
|
208
255
|
setHeading(headingId);
|
|
209
256
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
257
|
+
const target =
|
|
258
|
+
document.getElementById(headingId) || headingElements.find((el) => el.id === headingId);
|
|
259
|
+
|
|
260
|
+
if (!target?.scrollIntoView) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
265
|
+
|
|
266
|
+
if (!target.getClientRects().length) {
|
|
267
|
+
return;
|
|
213
268
|
}
|
|
214
269
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
270
|
+
const scrollMargin = parseFloat(getComputedStyle(target).scrollMarginTop) || 0;
|
|
271
|
+
const distance = Math.abs(target.getBoundingClientRect().top - scrollMargin);
|
|
272
|
+
|
|
273
|
+
lockObserver.current = true;
|
|
274
|
+
clickScroll.current = {
|
|
275
|
+
target,
|
|
276
|
+
scrollMargin,
|
|
277
|
+
closestDistance: distance,
|
|
278
|
+
reached: distance <= TARGET_TOLERANCE,
|
|
279
|
+
};
|
|
219
280
|
},
|
|
220
|
-
[headingElements],
|
|
281
|
+
[headingElements, releaseClickScroll],
|
|
221
282
|
);
|
|
222
283
|
|
|
223
284
|
useEffect(() => {
|
|
@@ -268,7 +329,10 @@ export function useActiveHeading(
|
|
|
268
329
|
});
|
|
269
330
|
|
|
270
331
|
const handleScroll = () => {
|
|
271
|
-
if (
|
|
332
|
+
if (clickScroll.current) {
|
|
333
|
+
trackClickScroll();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
272
336
|
|
|
273
337
|
if (isTopOfPage()) {
|
|
274
338
|
setHeading(headingElements[0]?.id || undefined);
|
|
@@ -281,7 +345,7 @@ export function useActiveHeading(
|
|
|
281
345
|
observer.disconnect();
|
|
282
346
|
window.removeEventListener('scroll', handleScroll);
|
|
283
347
|
};
|
|
284
|
-
}, [headingElements, intersectionCallback, getTriggerOffset]);
|
|
348
|
+
}, [headingElements, intersectionCallback, getTriggerOffset, trackClickScroll]);
|
|
285
349
|
|
|
286
350
|
return { heading, handleHeadingClick };
|
|
287
351
|
}
|
|
@@ -60,11 +60,13 @@ export function useActiveSectionId(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
window.addEventListener('scroll', scrollListener, { capture: false });
|
|
63
|
-
setTimeout(() => {
|
|
63
|
+
const initialRead = setTimeout(() => {
|
|
64
64
|
scrollListener();
|
|
65
65
|
}, 10);
|
|
66
66
|
|
|
67
67
|
return () => {
|
|
68
|
+
clearTimeout(initialRead);
|
|
69
|
+
scrollListener.cancel();
|
|
68
70
|
window.removeEventListener('scroll', scrollListener);
|
|
69
71
|
};
|
|
70
72
|
}, [location, heightOffset, scrollListener]);
|