@redocly/theme 0.59.0-rc.2 → 0.59.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.
Files changed (195) hide show
  1. package/LICENSE +7 -1
  2. package/lib/components/Buttons/AIAssistantButton.js +6 -2
  3. package/lib/components/Buttons/ConnectMCPButton.d.ts +8 -0
  4. package/lib/components/Buttons/ConnectMCPButton.js +145 -0
  5. package/lib/components/Buttons/variables.d.ts +1 -0
  6. package/lib/components/Buttons/variables.js +42 -2
  7. package/lib/components/Catalog/CatalogEntity/CatalogEntityInfoBar.js +1 -0
  8. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  9. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  10. package/lib/components/Catalog/CatalogEntityIcon.js +2 -1
  11. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +4 -0
  12. package/lib/components/Catalog/CatalogTagsWithTooltip.js +1 -1
  13. package/lib/components/Catalog/variables.js +1 -1
  14. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  15. package/lib/components/Dropdown/Dropdown.js +5 -5
  16. package/lib/components/Menu/MenuItem.js +1 -1
  17. package/lib/components/Navbar/NavbarItem.js +3 -3
  18. package/lib/components/PageActions/PageActions.js +4 -1
  19. package/lib/components/PageActions/variables.js +2 -0
  20. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +1 -2
  21. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  22. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  23. package/lib/components/Search/SearchAiConversationInput.d.ts +3 -1
  24. package/lib/components/Search/SearchAiConversationInput.js +39 -7
  25. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  26. package/lib/components/Search/SearchAiDialog.js +20 -9
  27. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  28. package/lib/components/Search/SearchAiMessage.js +146 -22
  29. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  30. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  31. package/lib/components/Search/SearchDialog.js +36 -5
  32. package/lib/components/Search/SearchGroups.js +2 -2
  33. package/lib/components/Search/variables.js +36 -64
  34. package/lib/components/Segmented/Segmented.d.ts +1 -8
  35. package/lib/components/Segmented/Segmented.js +3 -1
  36. package/lib/components/Select/SelectInput.js +1 -1
  37. package/lib/components/Select/variables.js +2 -2
  38. package/lib/components/Tag/Tag.d.ts +2 -1
  39. package/lib/components/Tag/Tag.js +66 -17
  40. package/lib/components/Tag/variables.dark.js +135 -36
  41. package/lib/components/Tag/variables.js +78 -61
  42. package/lib/core/constants/index.d.ts +1 -0
  43. package/lib/core/constants/index.js +1 -0
  44. package/lib/core/constants/mcp.d.ts +1 -0
  45. package/lib/core/constants/mcp.js +5 -0
  46. package/lib/core/constants/search.d.ts +5 -4
  47. package/lib/core/constants/search.js +4 -5
  48. package/lib/core/hooks/index.d.ts +3 -0
  49. package/lib/core/hooks/index.js +3 -0
  50. package/lib/core/hooks/menu/use-nested-menu.js +1 -1
  51. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  52. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  53. package/lib/core/hooks/use-connect-mcp-button.d.ts +13 -0
  54. package/lib/core/hooks/use-connect-mcp-button.js +50 -0
  55. package/lib/core/hooks/use-mcp-config.d.ts +9 -0
  56. package/lib/core/hooks/use-mcp-config.js +27 -0
  57. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  58. package/lib/core/hooks/use-page-actions.js +99 -119
  59. package/lib/core/hooks/use-product-picker.js +2 -1
  60. package/lib/core/hooks/use-tabs.d.ts +3 -2
  61. package/lib/core/hooks/use-tabs.js +115 -57
  62. package/lib/core/hooks/use-telemetry-fallback.d.ts +10 -8
  63. package/lib/core/hooks/use-telemetry-fallback.js +10 -8
  64. package/lib/core/openapi/index.d.ts +1 -0
  65. package/lib/core/styles/dark.js +4 -0
  66. package/lib/core/styles/global.js +5 -0
  67. package/lib/core/types/hooks.d.ts +2 -2
  68. package/lib/core/types/index.d.ts +1 -0
  69. package/lib/core/types/index.js +1 -0
  70. package/lib/core/types/l10n.d.ts +1 -1
  71. package/lib/core/types/mcp.d.ts +6 -0
  72. package/lib/core/types/mcp.js +3 -0
  73. package/lib/core/types/search.d.ts +11 -4
  74. package/lib/core/types/search.js +6 -0
  75. package/lib/core/types/segmented.d.ts +12 -0
  76. package/lib/core/types/segmented.js +3 -0
  77. package/lib/core/utils/frontmatter-translate.d.ts +6 -0
  78. package/lib/core/utils/frontmatter-translate.js +14 -0
  79. package/lib/core/utils/index.d.ts +2 -0
  80. package/lib/core/utils/index.js +2 -0
  81. package/lib/core/utils/mcp.d.ts +2 -0
  82. package/lib/core/utils/mcp.js +31 -0
  83. package/lib/icons/AiStarsGradientIcon/AiStarsGradientIcon.js +44 -4
  84. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  85. package/lib/icons/ConnectIcon/ConnectIcon.d.ts +9 -0
  86. package/lib/icons/ConnectIcon/ConnectIcon.js +17 -0
  87. package/lib/icons/CubeIcon/CubeIcon.d.ts +9 -0
  88. package/lib/icons/CubeIcon/CubeIcon.js +17 -0
  89. package/lib/icons/HashtagIcon/HashtagIcon.d.ts +9 -0
  90. package/lib/icons/HashtagIcon/HashtagIcon.js +22 -0
  91. package/lib/icons/RedoclyIcon/RedoclyIcon.js +4 -7
  92. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
  93. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
  94. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
  95. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
  96. package/lib/icons/VSCodeIcon/VSCodeIcon.d.ts +9 -0
  97. package/lib/icons/VSCodeIcon/VSCodeIcon.js +17 -0
  98. package/lib/index.d.ts +1 -2
  99. package/lib/index.js +1 -2
  100. package/lib/markdoc/components/Cards/Card.js +1 -28
  101. package/lib/markdoc/components/ConnectMCP/ConnectMCP.d.ts +8 -0
  102. package/lib/markdoc/components/ConnectMCP/ConnectMCP.js +19 -0
  103. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  104. package/lib/markdoc/components/Tabs/TabList.js +197 -47
  105. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  106. package/lib/markdoc/components/Tabs/Tabs.js +57 -12
  107. package/lib/markdoc/components/default.d.ts +1 -0
  108. package/lib/markdoc/components/default.js +1 -0
  109. package/lib/markdoc/default.d.ts +6 -0
  110. package/lib/markdoc/default.js +2 -0
  111. package/lib/markdoc/tags/card.js +0 -1
  112. package/lib/markdoc/tags/connect-mcp.d.ts +2 -0
  113. package/lib/markdoc/tags/connect-mcp.js +27 -0
  114. package/package.json +6 -6
  115. package/src/components/Buttons/AIAssistantButton.tsx +6 -2
  116. package/src/components/Buttons/ConnectMCPButton.tsx +180 -0
  117. package/src/components/Buttons/variables.ts +42 -1
  118. package/src/components/Catalog/CatalogEntity/CatalogEntityInfoBar.tsx +1 -0
  119. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  120. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  121. package/src/components/Catalog/CatalogEntityIcon.tsx +2 -1
  122. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +5 -0
  123. package/src/components/Catalog/CatalogTagsWithTooltip.tsx +1 -5
  124. package/src/components/Catalog/variables.ts +1 -1
  125. package/src/components/Dropdown/Dropdown.tsx +84 -79
  126. package/src/components/Menu/MenuItem.tsx +1 -0
  127. package/src/components/Navbar/NavbarItem.tsx +6 -5
  128. package/src/components/PageActions/PageActions.tsx +5 -1
  129. package/src/components/PageActions/variables.ts +2 -0
  130. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +3 -3
  131. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  132. package/src/components/Search/SearchAiConversationInput.tsx +61 -18
  133. package/src/components/Search/SearchAiDialog.tsx +52 -23
  134. package/src/components/Search/SearchAiMessage.tsx +172 -43
  135. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  136. package/src/components/Search/SearchDialog.tsx +49 -13
  137. package/src/components/Search/SearchGroups.tsx +2 -0
  138. package/src/components/Search/variables.ts +36 -64
  139. package/src/components/Segmented/Segmented.tsx +15 -20
  140. package/src/components/Select/SelectInput.tsx +1 -0
  141. package/src/components/Select/variables.ts +2 -2
  142. package/src/components/Tag/Tag.tsx +35 -19
  143. package/src/components/Tag/variables.dark.ts +135 -36
  144. package/src/components/Tag/variables.ts +78 -61
  145. package/src/core/constants/index.ts +1 -0
  146. package/src/core/constants/mcp.ts +1 -0
  147. package/src/core/constants/search.ts +8 -4
  148. package/src/core/hooks/index.ts +3 -0
  149. package/src/core/hooks/menu/use-nested-menu.ts +2 -2
  150. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  151. package/src/core/hooks/use-connect-mcp-button.ts +79 -0
  152. package/src/core/hooks/use-mcp-config.ts +43 -0
  153. package/src/core/hooks/use-page-actions.ts +141 -150
  154. package/src/core/hooks/use-product-picker.ts +2 -1
  155. package/src/core/hooks/use-tabs.ts +168 -86
  156. package/src/core/hooks/use-telemetry-fallback.ts +10 -8
  157. package/src/core/openapi/index.ts +1 -0
  158. package/src/core/styles/dark.ts +4 -0
  159. package/src/core/styles/global.ts +6 -1
  160. package/src/core/types/hooks.ts +5 -1
  161. package/src/core/types/index.ts +1 -0
  162. package/src/core/types/l10n.ts +13 -0
  163. package/src/core/types/mcp.ts +8 -0
  164. package/src/core/types/search.ts +13 -4
  165. package/src/core/types/segmented.ts +14 -0
  166. package/src/core/utils/frontmatter-translate.ts +9 -0
  167. package/src/core/utils/index.ts +2 -0
  168. package/src/core/utils/mcp.ts +34 -0
  169. package/src/icons/AiStarsGradientIcon/AiStarsGradientIcon.tsx +13 -4
  170. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  171. package/src/icons/ConnectIcon/ConnectIcon.tsx +27 -0
  172. package/src/icons/CubeIcon/CubeIcon.tsx +27 -0
  173. package/src/icons/HashtagIcon/HashtagIcon.tsx +23 -0
  174. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +4 -22
  175. package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
  176. package/src/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.tsx +35 -0
  177. package/src/icons/VSCodeIcon/VSCodeIcon.tsx +29 -0
  178. package/src/index.ts +1 -2
  179. package/src/markdoc/components/Cards/Card.tsx +1 -28
  180. package/src/markdoc/components/ConnectMCP/ConnectMCP.tsx +28 -0
  181. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  182. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
  183. package/src/markdoc/components/default.ts +1 -0
  184. package/src/markdoc/default.ts +2 -0
  185. package/src/markdoc/tags/card.ts +0 -1
  186. package/src/markdoc/tags/connect-mcp.ts +25 -0
  187. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.d.ts +0 -1
  188. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.js +0 -11
  189. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.d.ts +0 -1
  190. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.js +0 -5
  191. package/lib/ext/process-scorecard.d.ts +0 -5
  192. package/lib/ext/process-scorecard.js +0 -11
  193. package/src/components/OpenApiDocs/hooks/AdditionalOverviewInfo.tsx +0 -9
  194. package/src/components/OpenApiDocs/hooks/AfterOpenApiDescription.tsx +0 -1
  195. package/src/ext/process-scorecard.ts +0 -13
@@ -0,0 +1,32 @@
1
+ import { useState } from 'react';
2
+
3
+ import { useControl } from '../use-control';
4
+
5
+ type UseFeedbackTooltipReturn = {
6
+ isOpen: boolean;
7
+ showTooltip: () => void;
8
+ };
9
+
10
+ const DEFAULT_TOOLTIP_DURATION = 1500;
11
+
12
+ export function useFeedbackTooltip(): UseFeedbackTooltipReturn {
13
+ const [closeTooltipTimeout, setCloseTooltipTimeout] = useState<NodeJS.Timeout | null>(null);
14
+ const tooltipControl = useControl();
15
+
16
+ const showTooltip = () => {
17
+ tooltipControl.handleOpen();
18
+ if (closeTooltipTimeout) {
19
+ clearTimeout(closeTooltipTimeout);
20
+ }
21
+ const newCloseTooltipTimeout = setTimeout(() => {
22
+ tooltipControl.handleClose();
23
+ setCloseTooltipTimeout(null);
24
+ }, DEFAULT_TOOLTIP_DURATION);
25
+ setCloseTooltipTimeout(newCloseTooltipTimeout);
26
+ };
27
+
28
+ return {
29
+ isOpen: tooltipControl.isOpened,
30
+ showTooltip,
31
+ };
32
+ }
@@ -0,0 +1,79 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import debounce from 'lodash.debounce';
3
+
4
+ import type { MCPOption } from '../types';
5
+
6
+ import { useThemeHooks } from './use-theme-hooks';
7
+ import { useMCPConfig } from './use-mcp-config';
8
+ import { ClipboardService } from '../utils/clipboard-service';
9
+
10
+ export type McpButtonOptions = {
11
+ options?: MCPOption[];
12
+ };
13
+
14
+ export type McpButtonSettings = {
15
+ isCopied: boolean;
16
+ cursorUrl: string;
17
+ vscodeUrl: string;
18
+ triggerButtonText: string;
19
+ visibleOptions: MCPOption[];
20
+ handleAction: (action: MCPOption) => void;
21
+ };
22
+
23
+ const COPIED_RESET_TIMEOUT = 1000;
24
+
25
+ export function useConnectMCPButton({
26
+ options = ['cursor', 'vscode', 'copy'],
27
+ }: McpButtonOptions = {}): McpButtonSettings {
28
+ const { useTranslate } = useThemeHooks();
29
+ const { translate } = useTranslate();
30
+ const { serverName, serverUrl, cursorUrl, vscodeUrl } = useMCPConfig();
31
+ const [isCopied, setIsCopied] = useState(false);
32
+
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ const resetCopied = useCallback(
35
+ debounce(() => setIsCopied(false), COPIED_RESET_TIMEOUT),
36
+ [],
37
+ );
38
+
39
+ const handleAction = useCallback(
40
+ (action: MCPOption) => {
41
+ if (action === 'copy') {
42
+ const config = {
43
+ [serverName]: {
44
+ url: serverUrl,
45
+ description: 'MCP Server',
46
+ },
47
+ };
48
+ ClipboardService.copyCustom(JSON.stringify(config, null, 2));
49
+ setIsCopied(true);
50
+ resetCopied();
51
+ return;
52
+ }
53
+
54
+ const urlMap: Record<Exclude<MCPOption, 'copy'>, string> = {
55
+ cursor: cursorUrl,
56
+ vscode: vscodeUrl,
57
+ };
58
+
59
+ window.open(urlMap[action], '_blank');
60
+ },
61
+ [cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied],
62
+ );
63
+
64
+ const triggerButtonText = useMemo(
65
+ () => translate('page.actions.connectMcp', 'Connect MCP'),
66
+ [translate],
67
+ );
68
+
69
+ const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
70
+
71
+ return {
72
+ isCopied,
73
+ cursorUrl,
74
+ vscodeUrl,
75
+ triggerButtonText,
76
+ visibleOptions,
77
+ handleAction,
78
+ };
79
+ }
@@ -0,0 +1,43 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { useThemeConfig } from './use-theme-config';
4
+ import { IS_BROWSER } from '../utils/dom';
5
+ import { DEFAULT_MCP_SERVER_NAME } from '../constants';
6
+ import { generateMCPDeepLink } from '../utils/mcp';
7
+
8
+ export type McpConfig = {
9
+ serverName: string;
10
+ origin: string;
11
+ serverUrl: string;
12
+ cursorUrl: string;
13
+ vscodeUrl: string;
14
+ isMcpDisabled: boolean;
15
+ };
16
+
17
+ export function useMCPConfig(): McpConfig {
18
+ const themeConfig = useThemeConfig();
19
+
20
+ const origin = IS_BROWSER ? window.location.origin : (globalThis as any)['SSR_HOSTNAME'];
21
+ const serverName = themeConfig.mcp?.docs?.name || DEFAULT_MCP_SERVER_NAME;
22
+ const serverUrl = `${origin}/mcp`;
23
+ const isMcpDisabled = themeConfig.mcp?.hide || themeConfig.mcp?.docs?.hide || false;
24
+
25
+ const cursorUrl = useMemo(
26
+ () => generateMCPDeepLink('cursor', { serverName, url: serverUrl }),
27
+ [serverName, serverUrl],
28
+ );
29
+
30
+ const vscodeUrl = useMemo(
31
+ () => generateMCPDeepLink('vscode', { serverName, url: serverUrl }),
32
+ [serverName, serverUrl],
33
+ );
34
+
35
+ return {
36
+ serverName,
37
+ origin,
38
+ serverUrl,
39
+ cursorUrl,
40
+ vscodeUrl,
41
+ isMcpDisabled,
42
+ };
43
+ }
@@ -1,31 +1,40 @@
1
- import { useMemo } from 'react';
1
+ import { useMemo, useCallback } from 'react';
2
2
 
3
3
  import type { PageProps, UiAccessibleConfig } from '@redocly/config';
4
- import type { PageAction } from '../types';
4
+ import type { PageAction, MCPClientType, McpConnectionParams } from '../types';
5
5
 
6
6
  import { CopyIcon } from '@redocly/theme/icons/CopyIcon/CopyIcon';
7
7
  import { ChatGptIcon } from '@redocly/theme/icons/ChatGptIcon/ChatGptIcon';
8
8
  import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
9
9
  import { MarkdownFullIcon } from '@redocly/theme/icons/MarkdownFullIcon/MarkdownFullIcon';
10
+ import { VSCodeIcon } from '@redocly/theme/icons/VSCodeIcon/VSCodeIcon';
11
+ import { CursorIcon } from '@redocly/theme/icons/CursorIcon/CursorIcon';
10
12
 
11
13
  import { useThemeHooks } from './use-theme-hooks';
12
14
  import { useThemeConfig } from './use-theme-config';
15
+ import { useMCPConfig } from './use-mcp-config';
13
16
  import { ClipboardService } from '../utils/clipboard-service';
14
17
  import { IS_BROWSER } from '../utils/dom';
15
- import { CursorIcon } from '../../icons/CursorIcon/CursorIcon';
18
+ import { generateMCPDeepLink } from '../utils/mcp';
16
19
 
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';
20
+ const DEFAULT_ENABLED_ACTIONS = [
21
+ 'copy',
22
+ 'view',
23
+ 'chatgpt',
24
+ 'claude',
25
+ 'docs-mcp-cursor',
26
+ 'docs-mcp-vscode',
27
+ ] as const;
20
28
 
21
29
  export type PageActionType =
22
30
  | 'copy'
23
31
  | 'view'
24
32
  | 'chatgpt'
25
33
  | 'claude'
34
+ | 'docs-mcp-cursor'
35
+ | 'docs-mcp-vscode'
26
36
  | 'mcp-cursor'
27
- | 'mcp-claude'
28
- | 'mcp-json';
37
+ | 'mcp-vscode';
29
38
 
30
39
  export function usePageActions(
31
40
  pageSlug: string,
@@ -34,12 +43,12 @@ export function usePageActions(
34
43
  ): PageAction[] {
35
44
  const { useTranslate, usePageData, usePageProps, usePageSharedData } = useThemeHooks();
36
45
  const { translate } = useTranslate();
37
-
38
46
  const themeConfig = useThemeConfig();
39
47
  const pageProps = usePageProps();
40
48
  const openApiSharedData = usePageSharedData<
41
49
  { options: { excludeFromSearch: boolean } } | undefined
42
50
  >('openAPIDocsStore');
51
+ const mcpConfig = useMCPConfig();
43
52
 
44
53
  const shouldHideAllActions = shouldHidePageActions(
45
54
  pageProps,
@@ -48,172 +57,154 @@ export function usePageActions(
48
57
  );
49
58
  const { isPublic } = usePageData() || {};
50
59
 
60
+ const createMCPHandler = useCallback(
61
+ (clientType: MCPClientType, requiresMcpUrl: boolean = false) =>
62
+ () => {
63
+ if (requiresMcpUrl && !mcpUrl) return null;
64
+ if (!requiresMcpUrl && (mcpUrl || mcpConfig.isMcpDisabled)) return null;
65
+
66
+ const config = requiresMcpUrl
67
+ ? { serverName: mcpConfig.serverName, url: mcpUrl || '' }
68
+ : { serverName: mcpConfig.serverName, url: mcpConfig.serverUrl || '' };
69
+
70
+ return createMCPAction({ clientType, mcpConfig: config, translate });
71
+ },
72
+ [mcpUrl, mcpConfig, translate],
73
+ );
74
+
51
75
  const result: PageAction[] = useMemo(() => {
52
- if (shouldHideAllActions && !actions?.length) {
76
+ if (shouldHideAllActions) {
53
77
  return [];
54
78
  }
55
79
 
56
80
  const origin = IS_BROWSER ? window.location.origin : (globalThis as any)['SSR_HOSTNAME'];
57
-
58
81
  const normalizedSlug = pageSlug.startsWith('/') ? pageSlug : '/' + pageSlug;
59
-
60
82
  const mdPageUrl = new URL(
61
83
  origin + normalizedSlug + (normalizedSlug === '/' ? 'index.html.md' : '.md'),
62
84
  ).toString();
63
85
 
64
- function getExternalAiPromptLink(baseUrl: string): string {
65
- const externalAiPrompt = `Read ${mdPageUrl} and answer questions based on the content.`;
66
-
67
- const url = new URL(baseUrl);
68
- url.searchParams.set('q', externalAiPrompt);
69
-
70
- return url.toString();
71
- }
72
-
73
- return (themeConfig.navigation?.actions?.items || actions || DEFAULT_ENABLED_ACTIONS)
74
- .map((action) => {
75
- function generateMCPConfig(isCursor?: boolean): string {
76
- const jsonConfig = {
77
- 'mcp-server': {
78
- url: mcpUrl,
79
- description: 'MCP Server',
80
- },
81
- };
82
- if (isCursor) {
83
- const url = CURSOR_URL.replace('$NAME', 'mcp-server').replace(
84
- '$BASE64_ENCODED_CONFIG',
85
- btoa(JSON.stringify(jsonConfig['mcp-server'])),
86
- );
87
- return url;
86
+ const actionHandlers: Record<PageActionType, () => PageAction | null> = {
87
+ 'docs-mcp-cursor': createMCPHandler('cursor'),
88
+ 'docs-mcp-vscode': createMCPHandler('vscode'),
89
+ 'mcp-cursor': createMCPHandler('cursor', true),
90
+ 'mcp-vscode': createMCPHandler('vscode', true),
91
+
92
+ copy: () => ({
93
+ buttonText: translate('page.actions.copyButtonText', 'Copy'),
94
+ title: translate('page.actions.copyTitle', 'Copy for LLM'),
95
+ description: translate('page.actions.copyDescription', 'Copy page as Markdown for LLMs'),
96
+ iconComponent: CopyIcon,
97
+ onClick: async () => {
98
+ try {
99
+ const result = await fetch(mdPageUrl);
100
+ if (result.status !== 200) {
101
+ return;
102
+ }
103
+ const text = await result.text();
104
+ ClipboardService.copyCustom(text);
105
+ } catch (error) {
106
+ console.error(error);
88
107
  }
89
-
90
- return JSON.stringify(jsonConfig, null, 2);
108
+ },
109
+ }),
110
+
111
+ view: () => ({
112
+ buttonText: translate('page.actions.viewAsMdButtonText', 'View as Markdown'),
113
+ title: translate('page.actions.viewAsMdTitle', 'View as Markdown'),
114
+ description: translate('page.actions.viewAsMdDescription', 'Open this page as Markdown'),
115
+ iconComponent: MarkdownFullIcon,
116
+ link: mdPageUrl,
117
+ }),
118
+
119
+ chatgpt: () => {
120
+ if (!isPublic) {
121
+ return null;
91
122
  }
92
-
93
- switch (action) {
94
- case 'mcp-cursor':
95
- if (!mcpUrl) {
96
- return null;
97
- }
98
-
99
- return {
100
- buttonText: translate('page.actions.cursorMcpButtonText', 'Connect to Cursor'),
101
- title: translate('page.actions.cursorMcpTitle', 'Connect to Cursor'),
102
- description: translate(
103
- 'page.actions.cursorMcpDescription',
104
- 'Install MCP server on Cursor',
105
- ),
106
- iconComponent: CursorIcon,
107
- onClick: () => {
108
- try {
109
- const url = generateMCPConfig(true);
110
- window.open(url, '_blank');
111
- } catch (error) {
112
- console.error(error);
113
- }
114
- },
115
- };
116
- case 'mcp-claude':
117
- if (!mcpUrl) {
118
- return null;
119
- }
120
-
121
- return {
122
- buttonText: 'Copy Claude configuration',
123
- title: 'Copy Claude configuration',
124
- description: 'Copy MCP configuration for Claude',
125
- iconComponent: ClaudeIcon,
126
- onClick: async () => {
127
- ClipboardService.copyCustom(`claude mcp add --transport http server ${mcpUrl}`);
128
- },
129
- };
130
- case 'mcp-json':
131
- return {
132
- buttonText: 'Copy MCP configuration',
133
- title: 'Copy MCP JSON configuration',
134
- description: 'Copy MCP JSON configuration',
135
- iconComponent: CopyIcon,
136
- onClick: async () => {
137
- ClipboardService.copyCustom(generateMCPConfig());
138
- },
139
- };
140
- case 'copy':
141
- return {
142
- buttonText: translate('page.actions.copyButtonText', 'Copy'),
143
- title: translate('page.actions.copyTitle', 'Copy for LLM'),
144
- description: translate(
145
- 'page.actions.copyDescription',
146
- 'Copy page as Markdown for LLMs',
147
- ),
148
- iconComponent: CopyIcon,
149
- onClick: async () => {
150
- try {
151
- const result = await fetch(mdPageUrl);
152
- if (result.status !== 200) {
153
- throw new Error('Failed to fetch markdown content');
154
- }
155
- const text = await result.text();
156
-
157
- ClipboardService.copyCustom(text);
158
- } catch (error) {
159
- console.error(error);
160
- }
161
- },
162
- };
163
- case 'view':
164
- return {
165
- buttonText: translate('page.actions.viewAsMdButtonText', 'View as Markdown'),
166
- title: translate('page.actions.viewAsMdTitle', 'View as Markdown'),
167
- description: translate(
168
- 'page.actions.viewAsMdDescription',
169
- 'Open this page as Markdown',
170
- ),
171
- iconComponent: MarkdownFullIcon,
172
- link: mdPageUrl,
173
- };
174
- case 'chatgpt':
175
- if (!isPublic) {
176
- return null;
177
- }
178
-
179
- return {
180
- buttonText: translate('page.actions.chatGptButtonText', 'Open in ChatGPT'),
181
- title: translate('page.actions.chatGptTitle', 'Open in ChatGPT'),
182
- description: translate(
183
- 'page.actions.chatGptDescription',
184
- 'Get insights from ChatGPT',
185
- ),
186
- iconComponent: ChatGptIcon,
187
- link: getExternalAiPromptLink('https://chat.openai.com'),
188
- };
189
- case 'claude':
190
- if (!isPublic) {
191
- return null;
192
- }
193
-
194
- return {
195
- buttonText: translate('page.actions.claudeButtonText', 'Open in Claude'),
196
- title: translate('page.actions.claudeTitle', 'Open in Claude'),
197
- description: translate('page.actions.claudeDescription', 'Get insights from Claude'),
198
- iconComponent: ClaudeIcon,
199
- link: getExternalAiPromptLink('https://claude.ai/new'),
200
- };
123
+ return {
124
+ buttonText: translate('page.actions.chatGptButtonText', 'Open in ChatGPT'),
125
+ title: translate('page.actions.chatGptTitle', 'Open in ChatGPT'),
126
+ description: translate('page.actions.chatGptDescription', 'Get insights from ChatGPT'),
127
+ iconComponent: ChatGptIcon,
128
+ link: getExternalAiPromptLink('https://chat.openai.com', mdPageUrl),
129
+ };
130
+ },
131
+
132
+ claude: () => {
133
+ if (!isPublic) {
134
+ return null;
201
135
  }
202
- })
203
- .filter((action) => action !== null);
136
+ return {
137
+ buttonText: translate('page.actions.claudeButtonText', 'Open in Claude'),
138
+ title: translate('page.actions.claudeTitle', 'Open in Claude'),
139
+ description: translate('page.actions.claudeDescription', 'Get insights from Claude'),
140
+ iconComponent: ClaudeIcon,
141
+ link: getExternalAiPromptLink('https://claude.ai/new', mdPageUrl),
142
+ };
143
+ },
144
+ };
145
+
146
+ return (themeConfig.navigation?.actions?.items || actions || DEFAULT_ENABLED_ACTIONS)
147
+ .map((action) => actionHandlers[action]?.())
148
+ .filter((action): action is PageAction => action !== null);
204
149
  }, [
205
150
  shouldHideAllActions,
206
151
  pageSlug,
207
152
  themeConfig.navigation?.actions?.items,
208
153
  actions,
209
- mcpUrl,
210
154
  translate,
211
155
  isPublic,
156
+ createMCPHandler,
212
157
  ]);
213
158
 
214
159
  return result;
215
160
  }
216
161
 
162
+ function getExternalAiPromptLink(baseUrl: string, mdPageUrl: string): string {
163
+ const externalAiPrompt = `Read ${mdPageUrl} and answer questions based on the content.`;
164
+ const url = new URL(baseUrl);
165
+ url.searchParams.set('q', externalAiPrompt);
166
+ return url.toString();
167
+ }
168
+
169
+ type CreateMCPActionParams = {
170
+ clientType: MCPClientType;
171
+ mcpConfig: McpConnectionParams;
172
+ translate: (key: string, defaultValue: string) => string;
173
+ };
174
+
175
+ function createMCPAction({ clientType, mcpConfig, translate }: CreateMCPActionParams): PageAction {
176
+ const url = generateMCPDeepLink(clientType, mcpConfig);
177
+ const sharedProps = {
178
+ onClick: () => {
179
+ window.open(url, '_blank');
180
+ },
181
+ };
182
+
183
+ if (clientType === 'cursor') {
184
+ return {
185
+ ...sharedProps,
186
+ buttonText: translate('page.actions.connectMcp.cursor', 'Connect to Cursor'),
187
+ title: translate('page.actions.connectMcp.cursor', 'Connect to Cursor'),
188
+ description: translate(
189
+ 'page.actions.connectMcp.cursorDescription',
190
+ 'Install MCP server on Cursor',
191
+ ),
192
+ iconComponent: CursorIcon,
193
+ };
194
+ }
195
+
196
+ return {
197
+ ...sharedProps,
198
+ buttonText: translate('page.actions.connectMcp.vscode', 'Connect to VS Code'),
199
+ title: translate('page.actions.connectMcp.vscode', 'Connect to VS Code'),
200
+ description: translate(
201
+ 'page.actions.connectMcp.vscodeDescription',
202
+ 'Install MCP server on VS Code',
203
+ ),
204
+ iconComponent: VSCodeIcon,
205
+ };
206
+ }
207
+
217
208
  function shouldHidePageActions(
218
209
  pageProps: PageProps,
219
210
  themeConfig: UiAccessibleConfig,
@@ -1,6 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom';
2
2
 
3
3
  import { useThemeHooks } from './use-theme-hooks';
4
+ import { withPathPrefix } from '../utils';
4
5
 
5
6
  export function useProductPicker() {
6
7
  const { useCurrentProduct, useProducts, useTelemetry, useLoadAndNavigate } = useThemeHooks();
@@ -12,7 +13,7 @@ export function useProductPicker() {
12
13
  function setProduct(product: typeof currentProduct) {
13
14
  if (!product) return;
14
15
  telemetry.sendProductPickedMessage({ product: product.slug });
15
- loadAndNavigate({ navigate, to: product.link });
16
+ loadAndNavigate({ navigate, to: withPathPrefix(product.link) });
16
17
  }
17
18
  return {
18
19
  currentProduct,