@redocly/theme 0.59.0-next.10 → 0.59.0-next.12

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 (71) hide show
  1. package/lib/components/Buttons/ConnectMCPButton.d.ts +8 -0
  2. package/lib/components/Buttons/ConnectMCPButton.js +145 -0
  3. package/lib/components/Buttons/variables.d.ts +1 -0
  4. package/lib/components/Buttons/variables.js +41 -1
  5. package/lib/components/PageActions/PageActions.js +4 -1
  6. package/lib/components/PageActions/variables.js +2 -0
  7. package/lib/components/Segmented/Segmented.d.ts +1 -8
  8. package/lib/components/Segmented/Segmented.js +3 -1
  9. package/lib/core/constants/index.d.ts +1 -0
  10. package/lib/core/constants/index.js +1 -0
  11. package/lib/core/constants/mcp.d.ts +1 -0
  12. package/lib/core/constants/mcp.js +5 -0
  13. package/lib/core/hooks/index.d.ts +2 -0
  14. package/lib/core/hooks/index.js +2 -0
  15. package/lib/core/hooks/use-connect-mcp-button.d.ts +13 -0
  16. package/lib/core/hooks/use-connect-mcp-button.js +50 -0
  17. package/lib/core/hooks/use-mcp-config.d.ts +9 -0
  18. package/lib/core/hooks/use-mcp-config.js +27 -0
  19. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  20. package/lib/core/hooks/use-page-actions.js +98 -95
  21. package/lib/core/openapi/index.d.ts +1 -0
  22. package/lib/core/styles/global.js +1 -0
  23. package/lib/core/types/index.d.ts +1 -0
  24. package/lib/core/types/index.js +1 -0
  25. package/lib/core/types/l10n.d.ts +1 -1
  26. package/lib/core/types/mcp.d.ts +6 -0
  27. package/lib/core/types/mcp.js +3 -0
  28. package/lib/core/types/segmented.d.ts +12 -0
  29. package/lib/core/types/segmented.js +3 -0
  30. package/lib/core/utils/index.d.ts +1 -0
  31. package/lib/core/utils/index.js +1 -0
  32. package/lib/core/utils/mcp.d.ts +2 -0
  33. package/lib/core/utils/mcp.js +31 -0
  34. package/lib/icons/ConnectIcon/ConnectIcon.d.ts +9 -0
  35. package/lib/icons/ConnectIcon/ConnectIcon.js +17 -0
  36. package/lib/icons/VSCodeIcon/VSCodeIcon.d.ts +9 -0
  37. package/lib/icons/VSCodeIcon/VSCodeIcon.js +17 -0
  38. package/lib/markdoc/components/ConnectMCP/ConnectMCP.d.ts +8 -0
  39. package/lib/markdoc/components/ConnectMCP/ConnectMCP.js +19 -0
  40. package/lib/markdoc/components/default.d.ts +1 -0
  41. package/lib/markdoc/components/default.js +1 -0
  42. package/lib/markdoc/default.d.ts +6 -0
  43. package/lib/markdoc/default.js +2 -0
  44. package/lib/markdoc/tags/connect-mcp.d.ts +2 -0
  45. package/lib/markdoc/tags/connect-mcp.js +27 -0
  46. package/package.json +3 -3
  47. package/src/components/Buttons/ConnectMCPButton.tsx +180 -0
  48. package/src/components/Buttons/variables.ts +41 -0
  49. package/src/components/PageActions/PageActions.tsx +5 -1
  50. package/src/components/PageActions/variables.ts +2 -0
  51. package/src/components/Segmented/Segmented.tsx +15 -20
  52. package/src/core/constants/index.ts +1 -0
  53. package/src/core/constants/mcp.ts +1 -0
  54. package/src/core/hooks/index.ts +2 -0
  55. package/src/core/hooks/use-connect-mcp-button.ts +79 -0
  56. package/src/core/hooks/use-mcp-config.ts +43 -0
  57. package/src/core/hooks/use-page-actions.ts +148 -126
  58. package/src/core/openapi/index.ts +1 -0
  59. package/src/core/styles/global.ts +2 -1
  60. package/src/core/types/index.ts +1 -0
  61. package/src/core/types/l10n.ts +9 -0
  62. package/src/core/types/mcp.ts +8 -0
  63. package/src/core/types/segmented.ts +14 -0
  64. package/src/core/utils/index.ts +1 -0
  65. package/src/core/utils/mcp.ts +34 -0
  66. package/src/icons/ConnectIcon/ConnectIcon.tsx +27 -0
  67. package/src/icons/VSCodeIcon/VSCodeIcon.tsx +29 -0
  68. package/src/markdoc/components/ConnectMCP/ConnectMCP.tsx +28 -0
  69. package/src/markdoc/components/default.ts +1 -0
  70. package/src/markdoc/default.ts +2 -0
  71. package/src/markdoc/tags/connect-mcp.ts +25 -0
@@ -0,0 +1,180 @@
1
+ import React, { useMemo } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { MCPOption } from '@redocly/theme/core/types';
6
+
7
+ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
8
+ import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
9
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
10
+ import { Button } from '@redocly/theme/components/Button/Button';
11
+ import { CursorIcon } from '@redocly/theme/icons/CursorIcon/CursorIcon';
12
+ import { VSCodeIcon } from '@redocly/theme/icons/VSCodeIcon/VSCodeIcon';
13
+ import { CopyIcon } from '@redocly/theme/icons/CopyIcon/CopyIcon';
14
+ import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
15
+ import { ConnectIcon } from '@redocly/theme/icons/ConnectIcon/ConnectIcon';
16
+ import { useConnectMCPButton } from '@redocly/theme/core/hooks/use-connect-mcp-button';
17
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
18
+
19
+ type TriggerButtonProps = {
20
+ text: string;
21
+ };
22
+
23
+ const TriggerButton = React.memo(({ text }: TriggerButtonProps): JSX.Element => {
24
+ return (
25
+ <StyledButton variant="secondary" icon={<ConnectIcon />}>
26
+ {text}
27
+ </StyledButton>
28
+ );
29
+ });
30
+
31
+ type MenuOption = {
32
+ key: MCPOption;
33
+ icon: React.ComponentType;
34
+ titleTranslationKey: string;
35
+ titleDefault: string;
36
+ descriptionTranslationKey: string;
37
+ descriptionDefault: string;
38
+ };
39
+
40
+ const MENU_OPTIONS: MenuOption[] = [
41
+ {
42
+ key: 'cursor',
43
+ icon: CursorIcon,
44
+ titleTranslationKey: 'page.actions.connectMcp.cursor',
45
+ titleDefault: 'Connect to Cursor',
46
+ descriptionTranslationKey: 'page.actions.connectMcp.cursorDescription',
47
+ descriptionDefault: 'Install MCP server on Cursor',
48
+ },
49
+ {
50
+ key: 'vscode',
51
+ icon: VSCodeIcon,
52
+ titleTranslationKey: 'page.actions.connectMcp.vscode',
53
+ titleDefault: 'Connect to VS Code',
54
+ descriptionTranslationKey: 'page.actions.connectMcp.vscodeDescription',
55
+ descriptionDefault: 'Install MCP server on VS Code',
56
+ },
57
+ {
58
+ key: 'copy',
59
+ icon: CopyIcon,
60
+ titleTranslationKey: 'page.actions.connectMcp.copyConfig',
61
+ titleDefault: 'Copy MCP Configuration',
62
+ descriptionTranslationKey: 'page.actions.connectMcp.copyConfigDescription',
63
+ descriptionDefault: 'Copy MCP JSON Configuration',
64
+ },
65
+ ];
66
+
67
+ export type ConnectMCPButtonProps = {
68
+ placement?: 'top' | 'bottom';
69
+ alignment?: 'start' | 'end';
70
+ options?: MCPOption[];
71
+ };
72
+
73
+ export function ConnectMCPButton({
74
+ placement = 'bottom',
75
+ alignment = 'end',
76
+ options = ['cursor', 'vscode', 'copy'],
77
+ }: ConnectMCPButtonProps): JSX.Element {
78
+ const { useTranslate } = useThemeHooks();
79
+ const { translate } = useTranslate();
80
+
81
+ const { isCopied, triggerButtonText, visibleOptions, handleAction } = useConnectMCPButton({
82
+ options,
83
+ });
84
+
85
+ const menuOptions = useMemo(
86
+ () => MENU_OPTIONS.filter((option) => visibleOptions.includes(option.key)),
87
+ [visibleOptions],
88
+ );
89
+
90
+ return (
91
+ <ConnectMCPButtonWrapper data-component-name="Buttons/ConnectMCPButton">
92
+ <Dropdown
93
+ trigger={<TriggerButton text={triggerButtonText} />}
94
+ triggerEvent="hover"
95
+ placement={placement}
96
+ alignment={alignment}
97
+ closeOnClick={false}
98
+ >
99
+ <DropdownMenu>
100
+ {menuOptions.map((option) => {
101
+ const Icon = option.icon;
102
+ const showCheckmark = option.key === 'copy' && isCopied;
103
+
104
+ return (
105
+ <DropdownMenuItem key={option.key} onAction={() => handleAction(option.key)}>
106
+ <MenuItemContent>
107
+ <MenuItemIcon>
108
+ {showCheckmark ? (
109
+ <CheckmarkFilledIcon color="var(--color-success-base)" />
110
+ ) : (
111
+ <Icon />
112
+ )}
113
+ </MenuItemIcon>
114
+ <MenuItemText>
115
+ <MenuItemTitle>
116
+ {translate(option.titleTranslationKey, option.titleDefault)}
117
+ </MenuItemTitle>
118
+ <MenuItemDescription>
119
+ {translate(option.descriptionTranslationKey, option.descriptionDefault)}
120
+ </MenuItemDescription>
121
+ </MenuItemText>
122
+ </MenuItemContent>
123
+ </DropdownMenuItem>
124
+ );
125
+ })}
126
+ </DropdownMenu>
127
+ </Dropdown>
128
+ </ConnectMCPButtonWrapper>
129
+ );
130
+ }
131
+
132
+ const ConnectMCPButtonWrapper = styled.div`
133
+ display: inline-block;
134
+ position: relative;
135
+ `;
136
+
137
+ const StyledButton = styled(Button)`
138
+ --button-gap: var(--connect-mcp-button-gap);
139
+ `;
140
+
141
+ const MenuItemContent = styled.div`
142
+ display: flex;
143
+ align-items: center;
144
+ gap: var(--connect-mcp-button-menu-item-gap);
145
+ padding: var(--connect-mcp-button-menu-item-padding-block)
146
+ var(--connect-mcp-button-menu-item-padding-inline);
147
+ `;
148
+
149
+ const MenuItemIcon = styled.div`
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ width: var(--connect-mcp-button-menu-item-icon-size);
154
+ height: var(--connect-mcp-button-menu-item-icon-size);
155
+ flex-shrink: 0;
156
+ border: var(--connect-mcp-button-menu-item-icon-border);
157
+ border-radius: var(--connect-mcp-button-menu-item-icon-border-radius);
158
+ color: var(--connect-mcp-button-menu-item-icon-color);
159
+ `;
160
+
161
+ const MenuItemText = styled.div`
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: var(--connect-mcp-button-menu-item-text-gap);
165
+ flex: 1;
166
+ `;
167
+
168
+ const MenuItemTitle = styled.div`
169
+ font-size: var(--connect-mcp-button-menu-item-title-font-size);
170
+ font-weight: var(--connect-mcp-button-menu-item-title-font-weight);
171
+ line-height: var(--connect-mcp-button-menu-item-title-line-height);
172
+ color: var(--connect-mcp-button-menu-item-title-color);
173
+ `;
174
+
175
+ const MenuItemDescription = styled.div`
176
+ font-size: var(--connect-mcp-button-menu-item-description-font-size);
177
+ font-weight: var(--connect-mcp-button-menu-item-description-font-weight);
178
+ line-height: var(--connect-mcp-button-menu-item-description-line-height);
179
+ color: var(--connect-mcp-button-menu-item-description-color);
180
+ `;
@@ -46,3 +46,44 @@ export const aiAssistantButton = css`
46
46
  /* Transition */
47
47
  --ai-assistant-button-transition: box-shadow 0.3s ease, transform 0.2s ease;
48
48
  `;
49
+
50
+ export const connectMCPButton = css`
51
+ /**
52
+ * @tokens Connect MCP Button
53
+ * @presenter Color
54
+ */
55
+
56
+ /* Button gap */
57
+ --connect-mcp-button-gap: var(--spacing-xs);
58
+
59
+ /* Menu item layout */
60
+ --connect-mcp-button-menu-item-gap: var(--spacing-sm);
61
+ --connect-mcp-button-menu-item-padding-block: var(--spacing-xxs);
62
+ --connect-mcp-button-menu-item-padding-inline: 0;
63
+
64
+ /* Menu item icon */
65
+ --connect-mcp-button-menu-item-icon-size: 32px;
66
+ --connect-mcp-button-menu-item-icon-border-color: var(--border-color-secondary);
67
+ --connect-mcp-button-menu-item-icon-border-style: solid;
68
+ --connect-mcp-button-menu-item-icon-border-width: 1px;
69
+ --connect-mcp-button-menu-item-icon-border: var(--connect-mcp-button-menu-item-icon-border-width)
70
+ var(--connect-mcp-button-menu-item-icon-border-style)
71
+ var(--connect-mcp-button-menu-item-icon-border-color);
72
+ --connect-mcp-button-menu-item-icon-border-radius: var(--border-radius);
73
+ --connect-mcp-button-menu-item-icon-color: var(--icon-color-secondary);
74
+
75
+ /* Menu item text */
76
+ --connect-mcp-button-menu-item-text-gap: var(--spacing-xxs);
77
+
78
+ /* Menu item title */
79
+ --connect-mcp-button-menu-item-title-font-size: var(--font-size-base);
80
+ --connect-mcp-button-menu-item-title-font-weight: var(--font-weight-regular);
81
+ --connect-mcp-button-menu-item-title-line-height: var(--line-height-base);
82
+ --connect-mcp-button-menu-item-title-color: var(--text-color-secondary);
83
+
84
+ /* Menu item description */
85
+ --connect-mcp-button-menu-item-description-font-size: var(--font-size-sm);
86
+ --connect-mcp-button-menu-item-description-font-weight: var(--font-weight-regular);
87
+ --connect-mcp-button-menu-item-description-line-height: var(--line-height-sm);
88
+ --connect-mcp-button-menu-item-description-color: var(--text-color-description);
89
+ `;
@@ -77,7 +77,7 @@ export function PageActions(props: PageActionProps): JSX.Element | null {
77
77
  </Button>
78
78
  {actions.length > 1 ? (
79
79
  <Dropdown withArrow trigger={<Button />} placement="bottom" alignment="end">
80
- <DropdownMenu items={menuItems} />
80
+ <StyledDropdownMenu items={menuItems} />
81
81
  </Dropdown>
82
82
  ) : null}
83
83
  </ButtonGroup>
@@ -110,3 +110,7 @@ const LinkMenuItem = styled(Link)`
110
110
  text-decoration: none;
111
111
  --link-decoration-hover: none;
112
112
  `;
113
+
114
+ const StyledDropdownMenu = styled(DropdownMenu)`
115
+ --dropdown-menu-max-height: var(--page-actions-dropdown-max-height);
116
+ `;
@@ -7,6 +7,8 @@ export const pageActions = css`
7
7
  --page-actions-button-text-color: var(--text-color-secondary);
8
8
  --page-actions-button-padding: 5px 14px 5px var(--spacing-sm);
9
9
 
10
+ --page-actions-dropdown-max-height: fit-content;
11
+
10
12
  --page-actions-menu-item-padding: 3px 0;
11
13
  --page-actions-menu-item-gap: var(--spacing-xs);
12
14
  --page-actions-menu-item-icon-color: var(--icon-color-secondary);
@@ -2,18 +2,10 @@ import React, { forwardRef } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { ForwardedRef, ReactElement } from 'react';
5
- import type { SelectOption } from '@redocly/theme/core/types/select';
5
+ import type { SegmentedProps } from '@redocly/theme/core/types/segmented';
6
6
 
7
7
  import { typedMemo } from '@redocly/theme/core/hoc/typedMemo';
8
8
 
9
- export type SegmentedProps<T> = {
10
- options: SelectOption<T>[];
11
- value: T;
12
- onChange: ({ label, value }: SelectOption<T>) => void;
13
- className?: string;
14
- size?: 'regular' | 'small';
15
- };
16
-
17
9
  function SegmentedComponent<T>(
18
10
  { options, onChange, value, className = '', size = 'regular' }: SegmentedProps<T>,
19
11
  ref?: ForwardedRef<HTMLDivElement>,
@@ -25,17 +17,20 @@ function SegmentedComponent<T>(
25
17
  className={`tag-grey ${size} ${className}`}
26
18
  role="tablist"
27
19
  >
28
- {options.map((opt) => (
29
- <SegmentedItem
30
- key={opt.label}
31
- role="tab"
32
- title={opt.label}
33
- onClick={() => onChange(opt)}
34
- $isActive={value == opt.value}
35
- $size={size}
36
- >
37
- {opt.label}
38
- </SegmentedItem>
20
+ {options.map((opt, index) => (
21
+ <React.Fragment key={index}>
22
+ {opt.divider}
23
+ <SegmentedItem
24
+ key={opt.label}
25
+ role="tab"
26
+ title={opt.label}
27
+ onClick={() => onChange(opt)}
28
+ $isActive={value == opt.value}
29
+ $size={size}
30
+ >
31
+ {opt.element || opt.label}
32
+ </SegmentedItem>
33
+ </React.Fragment>
39
34
  ))}
40
35
  </SegmentedGroup>
41
36
  );
@@ -4,3 +4,4 @@ export * from './code-walkthrough';
4
4
  export * from './search';
5
5
  export * from './catalog';
6
6
  export * from './breadcrumb';
7
+ export * from './mcp';
@@ -0,0 +1 @@
1
+ export const DEFAULT_MCP_SERVER_NAME = 'mcp-server';
@@ -43,3 +43,5 @@ export * from './use-active-page-version';
43
43
  export * from './use-page-versions';
44
44
  export * from './use-user-teams';
45
45
  export * from './use-page-actions';
46
+ export * from './use-mcp-config';
47
+ export * from './use-connect-mcp-button';
@@ -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
+ }