@redocly/theme 0.69.0-next.1 → 0.69.0-next.11

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 (280) hide show
  1. package/lib/components/Accordion/Accordion.js +5 -1
  2. package/lib/components/Accordion/variables.js +1 -1
  3. package/lib/components/Admonition/Admonition.d.ts +1 -1
  4. package/lib/components/Admonition/Admonition.js +1 -1
  5. package/lib/components/Admonition/variables.dark.js +2 -2
  6. package/lib/components/Admonition/variables.js +10 -10
  7. package/lib/components/Badge/Badge.d.ts +3 -1
  8. package/lib/components/Badge/Badge.js +7 -2
  9. package/lib/components/Banner/variables.dark.js +2 -0
  10. package/lib/components/Banner/variables.js +3 -1
  11. package/lib/components/Breadcrumbs/Breadcrumb.js +1 -1
  12. package/lib/components/Breadcrumbs/variables.js +1 -1
  13. package/lib/components/Button/ButtonGroup.d.ts +3 -5
  14. package/lib/components/Button/ButtonGroup.js +25 -5
  15. package/lib/components/Button/variables.js +27 -4
  16. package/lib/components/Buttons/ConnectMCPButton.js +39 -123
  17. package/lib/components/Buttons/CopyButton.js +12 -1
  18. package/lib/components/Buttons/variables.d.ts +0 -1
  19. package/lib/components/Buttons/variables.js +3 -44
  20. package/lib/components/Catalog/CatalogEntities.js +1 -0
  21. package/lib/components/Catalog/CatalogEntity/CatalogEntityLinks.js +1 -1
  22. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +1 -1
  23. package/lib/components/Catalog/CatalogEntity/CatalogEntityMethodAndPath.js +1 -1
  24. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +5 -3
  25. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +1 -1
  26. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +1 -0
  27. package/lib/components/Catalog/CatalogViewModeToggle.d.ts +1 -1
  28. package/lib/components/Catalog/CatalogViewModeToggle.js +2 -43
  29. package/lib/components/Catalog/variables.js +5 -5
  30. package/lib/components/CatalogClassic/variables.js +2 -2
  31. package/lib/components/CodeBlock/CodeBlock.js +2 -1
  32. package/lib/components/CodeBlock/CodeBlockContainer.js +1 -0
  33. package/lib/components/Dropdown/Dropdown.d.ts +10 -20
  34. package/lib/components/Dropdown/Dropdown.js +13 -2
  35. package/lib/components/Dropdown/DropdownMenuItemContent.d.ts +9 -0
  36. package/lib/components/Dropdown/DropdownMenuItemContent.js +56 -0
  37. package/lib/components/Dropdown/variables.js +19 -2
  38. package/lib/components/Feedback/Mood.js +18 -14
  39. package/lib/components/Feedback/Rating.js +18 -13
  40. package/lib/components/Feedback/Scale.js +27 -22
  41. package/lib/components/Feedback/Sentiment.js +17 -13
  42. package/lib/components/Filter/FilterInput.js +4 -2
  43. package/lib/components/Filter/FilterSelect.js +1 -0
  44. package/lib/components/Filter/variables.js +2 -2
  45. package/lib/components/Image/variables.js +5 -3
  46. package/lib/components/JsonViewer/Helpers.js +12 -42
  47. package/lib/components/LanguagePicker/variables.js +1 -1
  48. package/lib/components/MCP/McpConnectionTabs.d.ts +7 -0
  49. package/lib/components/MCP/McpConnectionTabs.js +161 -0
  50. package/lib/components/Markdown/styles/base-table.js +8 -0
  51. package/lib/components/Markdown/variables.js +1 -1
  52. package/lib/components/Menu/MenuItem.js +1 -1
  53. package/lib/components/Menu/variables.js +1 -1
  54. package/lib/components/Navbar/variables.js +1 -1
  55. package/lib/components/PageActions/PageActions.js +4 -6
  56. package/lib/components/PageActions/PageActionsMenuItem.d.ts +4 -4
  57. package/lib/components/PageActions/PageActionsMenuItem.js +27 -43
  58. package/lib/components/Panel/Panel.js +5 -1
  59. package/lib/components/Panel/variables.js +1 -1
  60. package/lib/components/Product/variables.js +2 -1
  61. package/lib/components/ReplayGate/ReplayGate.d.ts +1 -0
  62. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +22 -32
  63. package/lib/components/Search/SearchAiDialog.js +4 -1
  64. package/lib/components/Search/SearchAiMessage.js +1 -1
  65. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +2 -2
  66. package/lib/components/Search/SearchDialog.js +2 -2
  67. package/lib/components/Search/SearchItem.js +6 -3
  68. package/lib/components/Search/SearchRecent.js +1 -0
  69. package/lib/components/Search/SearchSuggestedPages.js +1 -0
  70. package/lib/components/Search/variables.js +9 -5
  71. package/lib/components/Segmented/Segmented.js +3 -3
  72. package/lib/components/Select/Select.js +30 -23
  73. package/lib/components/Select/variables.js +3 -3
  74. package/lib/components/Sidebar/Sidebar.js +2 -38
  75. package/lib/components/SplitButton/SplitButton.d.ts +21 -0
  76. package/lib/components/SplitButton/SplitButton.js +37 -0
  77. package/lib/components/Switch/variables.dark.js +2 -2
  78. package/lib/components/Switch/variables.js +2 -2
  79. package/lib/components/Tag/variables.js +2 -2
  80. package/lib/components/Tags/variables.js +1 -1
  81. package/lib/components/Toast/variables.js +1 -1
  82. package/lib/components/Tooltip/AnchorTooltip.js +2 -2
  83. package/lib/components/Tooltip/JsTooltip.js +2 -2
  84. package/lib/components/UserMenu/variables.js +1 -1
  85. package/lib/core/constants/mcp.d.ts +4 -0
  86. package/lib/core/constants/mcp.js +10 -1
  87. package/lib/core/constants/search.js +2 -2
  88. package/lib/core/contexts/AccordionGroupContext.d.ts +1 -0
  89. package/lib/core/contexts/AccordionGroupContext.js +6 -0
  90. package/lib/core/contexts/PageActionsContext.d.ts +5 -0
  91. package/lib/core/contexts/PageActionsContext.js +10 -0
  92. package/lib/core/contexts/index.d.ts +2 -0
  93. package/lib/core/contexts/index.js +2 -0
  94. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +5 -17
  95. package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +2 -0
  96. package/lib/core/hooks/menu/use-menu-item-expanded.js +1 -0
  97. package/lib/core/hooks/use-active-heading.js +63 -15
  98. package/lib/core/hooks/use-active-section-id.js +3 -1
  99. package/lib/core/hooks/use-connect-mcp-button.d.ts +1 -1
  100. package/lib/core/hooks/use-connect-mcp-button.js +23 -15
  101. package/lib/core/hooks/use-control.js +3 -0
  102. package/lib/core/hooks/use-local-state.js +1 -0
  103. package/lib/core/hooks/use-mcp-config.d.ts +2 -0
  104. package/lib/core/hooks/use-mcp-config.js +14 -4
  105. package/lib/core/hooks/use-mobile-menu.js +10 -2
  106. package/lib/core/hooks/use-tabs.js +4 -2
  107. package/lib/core/hooks/use-telemetry-fallback.d.ts +2 -0
  108. package/lib/core/hooks/use-telemetry-fallback.js +2 -0
  109. package/lib/core/styles/global.js +16 -11
  110. package/lib/core/styles/palette.js +2 -0
  111. package/lib/core/types/button-group.d.ts +2 -0
  112. package/lib/core/types/button-group.js +3 -0
  113. package/lib/core/types/catalog.d.ts +2 -4
  114. package/lib/core/types/common.d.ts +2 -0
  115. package/lib/core/types/hooks.d.ts +1 -0
  116. package/lib/core/types/index.d.ts +1 -0
  117. package/lib/core/types/l10n.d.ts +1 -1
  118. package/lib/core/types/mcp.d.ts +1 -1
  119. package/lib/core/utils/RenderWithLink.d.ts +2 -0
  120. package/lib/core/utils/RenderWithLink.js +20 -0
  121. package/lib/core/utils/index.d.ts +1 -0
  122. package/lib/core/utils/index.js +1 -0
  123. package/lib/core/utils/mcp.d.ts +5 -0
  124. package/lib/core/utils/mcp.js +35 -0
  125. package/lib/icons/CodexIcon/CodexIcon.d.ts +25 -0
  126. package/lib/icons/CodexIcon/CodexIcon.js +22 -0
  127. package/lib/icons/LinkIcon/LinkIcon.js +1 -1
  128. package/lib/index.d.ts +2 -0
  129. package/lib/index.js +2 -0
  130. package/lib/layouts/McpNotAllowed.js +10 -10
  131. package/lib/markdoc/components/Accordion/Accordion.js +6 -6
  132. package/lib/markdoc/components/Accordion/AccordionGroup.d.ts +2 -1
  133. package/lib/markdoc/components/Accordion/AccordionGroup.js +39 -3
  134. package/lib/markdoc/components/Cards/Card.d.ts +8 -1
  135. package/lib/markdoc/components/Cards/Card.js +55 -16
  136. package/lib/markdoc/components/Cards/CardBadge.d.ts +9 -0
  137. package/lib/markdoc/components/Cards/CardBadge.js +30 -0
  138. package/lib/markdoc/components/Cards/CardIcon.d.ts +5 -4
  139. package/lib/markdoc/components/Cards/CardIcon.js +6 -3
  140. package/lib/markdoc/components/Cards/variables.dark.js +32 -0
  141. package/lib/markdoc/components/Cards/variables.js +81 -7
  142. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +1 -0
  143. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +1 -1
  144. package/lib/markdoc/components/CodeWalkthrough/variables.js +2 -1
  145. package/lib/markdoc/components/Heading/Heading.js +5 -2
  146. package/lib/markdoc/components/Tabs/TabList.js +6 -2
  147. package/lib/markdoc/components/Tabs/Tabs.js +4 -2
  148. package/lib/markdoc/components/Tabs/variables.js +4 -4
  149. package/lib/markdoc/tags/accordion-group.js +3 -0
  150. package/lib/markdoc/tags/admonition.js +1 -1
  151. package/lib/markdoc/tags/card.js +14 -0
  152. package/lib/markdoc/tags/connect-mcp.js +2 -1
  153. package/package.json +19 -10
  154. package/src/components/Accordion/Accordion.tsx +7 -3
  155. package/src/components/Accordion/variables.ts +1 -1
  156. package/src/components/Admonition/Admonition.tsx +2 -2
  157. package/src/components/Admonition/variables.dark.ts +2 -2
  158. package/src/components/Admonition/variables.ts +10 -10
  159. package/src/components/Badge/Badge.tsx +16 -2
  160. package/src/components/Banner/variables.dark.ts +2 -0
  161. package/src/components/Banner/variables.ts +3 -1
  162. package/src/components/Breadcrumbs/Breadcrumb.tsx +1 -1
  163. package/src/components/Breadcrumbs/variables.ts +1 -1
  164. package/src/components/Button/ButtonGroup.tsx +28 -10
  165. package/src/components/Button/variables.ts +27 -4
  166. package/src/components/Buttons/ConnectMCPButton.tsx +88 -145
  167. package/src/components/Buttons/CopyButton.tsx +14 -2
  168. package/src/components/Buttons/variables.ts +2 -44
  169. package/src/components/Catalog/CatalogEntities.tsx +1 -0
  170. package/src/components/Catalog/CatalogEntity/CatalogEntityLinks.tsx +1 -1
  171. package/src/components/Catalog/CatalogEntity/CatalogEntityMetadata.tsx +1 -1
  172. package/src/components/Catalog/CatalogEntity/CatalogEntityMethodAndPath.tsx +1 -1
  173. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.tsx +6 -4
  174. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +1 -1
  175. package/src/components/Catalog/CatalogFilter/CatalogFilterSelect.tsx +1 -0
  176. package/src/components/Catalog/CatalogViewModeToggle.tsx +2 -17
  177. package/src/components/Catalog/variables.ts +5 -5
  178. package/src/components/CatalogClassic/variables.ts +2 -2
  179. package/src/components/CodeBlock/CodeBlock.tsx +2 -1
  180. package/src/components/CodeBlock/CodeBlockContainer.tsx +1 -0
  181. package/src/components/Dropdown/Dropdown.tsx +25 -4
  182. package/src/components/Dropdown/DropdownMenuItemContent.tsx +72 -0
  183. package/src/components/Dropdown/variables.ts +19 -2
  184. package/src/components/Feedback/Mood.tsx +22 -15
  185. package/src/components/Feedback/Rating.tsx +22 -14
  186. package/src/components/Feedback/Scale.tsx +40 -32
  187. package/src/components/Feedback/Sentiment.tsx +21 -14
  188. package/src/components/Filter/FilterInput.tsx +5 -3
  189. package/src/components/Filter/FilterSelect.tsx +1 -0
  190. package/src/components/Filter/variables.ts +2 -2
  191. package/src/components/Image/variables.ts +5 -3
  192. package/src/components/JsonViewer/Helpers.tsx +13 -9
  193. package/src/components/LanguagePicker/variables.ts +1 -1
  194. package/src/components/MCP/McpConnectionTabs.tsx +285 -0
  195. package/src/components/Markdown/styles/base-table.ts +8 -0
  196. package/src/components/Markdown/variables.ts +1 -1
  197. package/src/components/Menu/MenuItem.tsx +2 -2
  198. package/src/components/Menu/variables.ts +1 -1
  199. package/src/components/Navbar/variables.ts +1 -1
  200. package/src/components/PageActions/PageActions.tsx +22 -23
  201. package/src/components/PageActions/PageActionsMenuItem.tsx +41 -55
  202. package/src/components/Panel/Panel.tsx +7 -3
  203. package/src/components/Panel/variables.ts +1 -1
  204. package/src/components/Product/variables.ts +2 -1
  205. package/src/components/ReplayGate/ReplayGate.tsx +3 -0
  206. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +32 -40
  207. package/src/components/Search/SearchAiDialog.tsx +4 -1
  208. package/src/components/Search/SearchAiMessage.tsx +1 -1
  209. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +2 -2
  210. package/src/components/Search/SearchDialog.tsx +2 -2
  211. package/src/components/Search/SearchItem.tsx +7 -1
  212. package/src/components/Search/SearchRecent.tsx +1 -0
  213. package/src/components/Search/SearchSuggestedPages.tsx +1 -0
  214. package/src/components/Search/variables.ts +9 -5
  215. package/src/components/Segmented/Segmented.tsx +6 -3
  216. package/src/components/Select/Select.tsx +36 -22
  217. package/src/components/Select/variables.ts +3 -3
  218. package/src/components/Sidebar/Sidebar.tsx +2 -6
  219. package/src/components/SplitButton/SplitButton.tsx +95 -0
  220. package/src/components/Switch/variables.dark.ts +2 -2
  221. package/src/components/Switch/variables.ts +2 -2
  222. package/src/components/Tag/variables.ts +2 -2
  223. package/src/components/Tags/variables.ts +1 -1
  224. package/src/components/Toast/variables.ts +1 -1
  225. package/src/components/Tooltip/AnchorTooltip.tsx +2 -2
  226. package/src/components/Tooltip/JsTooltip.tsx +2 -2
  227. package/src/components/UserMenu/variables.ts +1 -1
  228. package/src/core/constants/mcp.ts +14 -0
  229. package/src/core/constants/search.ts +2 -2
  230. package/src/core/contexts/AccordionGroupContext.tsx +3 -0
  231. package/src/core/contexts/PageActionsContext.tsx +7 -0
  232. package/src/core/contexts/index.ts +2 -0
  233. package/src/core/hooks/__mocks__/use-theme-hooks.ts +2 -0
  234. package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +6 -20
  235. package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +2 -0
  236. package/src/core/hooks/menu/use-menu-item-expanded.ts +1 -0
  237. package/src/core/hooks/use-active-heading.ts +83 -15
  238. package/src/core/hooks/use-active-section-id.ts +3 -1
  239. package/src/core/hooks/use-connect-mcp-button.ts +31 -17
  240. package/src/core/hooks/use-control.ts +5 -1
  241. package/src/core/hooks/use-local-state.ts +1 -0
  242. package/src/core/hooks/use-mcp-config.ts +15 -4
  243. package/src/core/hooks/use-mobile-menu.ts +11 -3
  244. package/src/core/hooks/use-tabs.ts +4 -2
  245. package/src/core/hooks/use-telemetry-fallback.ts +2 -0
  246. package/src/core/openapi/index.ts +1 -1
  247. package/src/core/styles/global.ts +17 -12
  248. package/src/core/styles/palette.ts +2 -0
  249. package/src/core/types/button-group.ts +2 -0
  250. package/src/core/types/catalog.ts +2 -4
  251. package/src/core/types/common.ts +3 -0
  252. package/src/core/types/hooks.ts +1 -0
  253. package/src/core/types/index.ts +1 -0
  254. package/src/core/types/l10n.ts +29 -1
  255. package/src/core/types/mcp.ts +1 -1
  256. package/src/core/utils/RenderWithLink.tsx +19 -0
  257. package/src/core/utils/index.ts +1 -0
  258. package/src/core/utils/mcp.ts +39 -0
  259. package/src/icons/CodexIcon/CodexIcon.tsx +23 -0
  260. package/src/icons/LinkIcon/LinkIcon.tsx +1 -1
  261. package/src/index.ts +2 -0
  262. package/src/layouts/McpNotAllowed.tsx +37 -11
  263. package/src/markdoc/components/Accordion/Accordion.tsx +8 -2
  264. package/src/markdoc/components/Accordion/AccordionGroup.tsx +9 -2
  265. package/src/markdoc/components/Cards/Card.tsx +79 -14
  266. package/src/markdoc/components/Cards/CardBadge.tsx +39 -0
  267. package/src/markdoc/components/Cards/CardIcon.tsx +16 -5
  268. package/src/markdoc/components/Cards/variables.dark.ts +32 -0
  269. package/src/markdoc/components/Cards/variables.ts +81 -7
  270. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +1 -0
  271. package/src/markdoc/components/CodeWalkthrough/CodeStep.tsx +1 -1
  272. package/src/markdoc/components/CodeWalkthrough/variables.ts +2 -1
  273. package/src/markdoc/components/Heading/Heading.tsx +7 -4
  274. package/src/markdoc/components/Tabs/TabList.tsx +6 -2
  275. package/src/markdoc/components/Tabs/Tabs.tsx +4 -2
  276. package/src/markdoc/components/Tabs/variables.ts +4 -4
  277. package/src/markdoc/tags/accordion-group.ts +3 -0
  278. package/src/markdoc/tags/admonition.ts +1 -1
  279. package/src/markdoc/tags/card.ts +14 -0
  280. package/src/markdoc/tags/connect-mcp.ts +3 -1
@@ -254,7 +254,7 @@ const TooltipBody = styled.span<{
254
254
  word-break: normal;
255
255
  overflow-wrap: break-word;
256
256
 
257
- border-radius: var(--border-radius-md);
257
+ border-radius: var(--border-radius-lg);
258
258
  transition: opacity 0.3s ease-out;
259
259
 
260
260
  font-size: var(--font-size-base);
@@ -274,7 +274,7 @@ const TooltipBody = styled.span<{
274
274
 
275
275
  background: var(--tooltip-bg-color);
276
276
  color: var(--tooltip-text-color);
277
- border: var(--tooltip-border-width, 0) var(--tooltip-border-style, solid)
277
+ border: var(--tooltip-border-width, 2px) var(--tooltip-border-style, solid)
278
278
  var(--tooltip-border-color, transparent);
279
279
  box-shadow: var(--bg-raised-shadow);
280
280
 
@@ -49,7 +49,7 @@ export const userMenu = css`
49
49
  --user-menu-dropdown-item-padding-vertical: var(--dropdown-menu-item-padding-vertical); // @presenter Spacing
50
50
  --user-menu-dropdown-item-separator-padding-top: var(--dropdown-menu-item-separator-padding-top ); // @presenter Spacing
51
51
  --user-menu-dropdown-item-separator-padding-bottom: var(--dropdown-menu-item-separator-padding-bottom ); // @presenter Spacing
52
- --user-menu-dropdown-item-border-radius: var(--dropdown-menu-item-border-radius ); // @presenter BorderRadius
52
+ --user-menu-dropdown-item-border-radius: var(--dropdown-menu-item-border-radius); // @presenter BorderRadius
53
53
  --user-menu-dropdown-item-bg-color-active: var(--dropdown-menu-item-bg-color-active); // @presenter Color
54
54
  --user-menu-dropdown-item-bg-color-hover: var(--dropdown-menu-item-bg-color-hover); // @presenter Color
55
55
  --user-menu-dropdown-item-bg-color-disabled: var(--dropdown-menu-item-bg-color-disabled); // @presenter Color
@@ -1 +1,15 @@
1
+ import type { MCPOption } from '../types/mcp';
2
+
1
3
  export const DEFAULT_MCP_SERVER_NAME = 'mcp-server';
4
+
5
+ export const MCP_ROUTE = '/mcp';
6
+
7
+ export const PUBLIC_MCP_ROUTE = '/mcp-public';
8
+
9
+ export const DEFAULT_MCP_OPTIONS: MCPOption[] = [
10
+ 'cursor',
11
+ 'vscode',
12
+ 'claude-code',
13
+ 'codex',
14
+ 'copy',
15
+ ];
@@ -29,13 +29,13 @@ export const AI_SEARCH_ERROR_CONFIG: Record<AiSearchError, AiSearchErrorConfig>
29
29
  headerKey: 'search.ai.error.header.forbidden',
30
30
  headerDefault: 'Feature Unavailable',
31
31
  messageKey: 'search.ai.error.description.forbidden',
32
- messageDefault: 'AI Search is currently unavailable.',
32
+ messageDefault: 'AI assistant is currently unavailable.',
33
33
  },
34
34
  [AiSearchError.Unauthorized]: {
35
35
  headerKey: 'search.ai.error.header.unauthorized',
36
36
  headerDefault: 'Authentication Required',
37
37
  messageKey: 'search.ai.error.description.unauthorized',
38
- messageDefault: 'You are not authorized to use AI Search.',
38
+ messageDefault: 'You are not authorized to use AI assistant.',
39
39
  },
40
40
  [AiSearchError.HttpRequestFailed]: defaultErrorConfig,
41
41
  [AiSearchError.EmptyResponse]: defaultErrorConfig,
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react';
2
+
3
+ export const AccordionGroupContext = createContext<string | null>(null);
@@ -0,0 +1,7 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * Whether the first heading inside the enclosing `Markdown` mounts the page actions.
5
+ * Without a provider `Heading` falls back to the page type in the page metadata.
6
+ */
7
+ export const PageActionsContext = createContext<boolean | null>(null);
@@ -1,7 +1,9 @@
1
1
  export * from './ThemeDataContext';
2
2
  export * from './MarkdownLinkContext';
3
+ export * from './PageActionsContext';
3
4
  export * from './CodeWalkthrough/CodeWalkthroughControlsContext';
4
5
  export * from './CodeWalkthrough/CodeWalkthroughStepsContext';
6
+ export * from './AccordionGroupContext';
5
7
  export * from './CodeSnippetContext';
6
8
  export * from './SearchContext';
7
9
  export * from './Toast/ToastContext';
@@ -18,6 +18,8 @@ export const useThemeHooks = vi.fn(() => ({
18
18
  useTelemetry: vi.fn(() => ({
19
19
  send: vi.fn(),
20
20
  sendCodeSnippetReportedMessage: vi.fn(),
21
+ sendConnectMcpClickedMessage: vi.fn(),
22
+ sendMcpConnectionSnippetCopiedMessage: vi.fn(),
21
23
  sendPageActionsButtonClickedMessage: vi.fn(),
22
24
  sendColorModeSwitchedMessage: vi.fn(),
23
25
  })),
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo, useRef } from 'react';
1
+ import { useState, useEffect, useMemo } from 'react';
2
2
  import { useLocation, useNavigate } from 'react-router';
3
3
 
4
4
  import type {
@@ -50,10 +50,6 @@ export function useCodeWalkthroughControls(
50
50
  const navigate = useNavigate();
51
51
  const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
52
52
 
53
- const filtersRef = useRef(filters);
54
- const inputsRef = useRef(inputs);
55
- const togglesRef = useRef(toggles);
56
-
57
53
  const getInitialState = () => {
58
54
  const state: CodeWalkthroughControlsState = {};
59
55
 
@@ -90,20 +86,11 @@ export function useCodeWalkthroughControls(
90
86
 
91
87
  const [controlsState, setControlsState] = useState(getInitialState);
92
88
 
93
- useEffect(() => {
94
- const sameProps = [
95
- JSON.stringify(filters) === JSON.stringify(filtersRef.current),
96
- JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
97
- JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
98
- ];
99
-
100
- if (sameProps.every(Boolean)) {
101
- return;
102
- }
89
+ const controlsKey = JSON.stringify([filters, inputs, toggles]);
90
+ const [prevControlsKey, setPrevControlsKey] = useState(controlsKey);
103
91
 
104
- filtersRef.current = filters;
105
- inputsRef.current = inputs;
106
- togglesRef.current = toggles;
92
+ if (prevControlsKey !== controlsKey) {
93
+ setPrevControlsKey(controlsKey);
107
94
 
108
95
  const newState = getInitialState();
109
96
 
@@ -120,8 +107,7 @@ export function useCodeWalkthroughControls(
120
107
  });
121
108
 
122
109
  setControlsState(newState);
123
- // eslint-disable-next-line react-hooks/exhaustive-deps
124
- }, [filters, inputs, toggles, enableDeepLink]);
110
+ }
125
111
 
126
112
  const changeControlState = (id: string, value: string | boolean) => {
127
113
  setControlsState((prev) => {
@@ -95,6 +95,7 @@ export function useCodeWalkthroughSteps({
95
95
  });
96
96
  }
97
97
 
98
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- marker geometry is derived from post-layout measurements (offsetTop, clientHeight, window.innerHeight)
98
99
  setMarkers(
99
100
  markers.reduce(
100
101
  (acc, marker, index) => {
@@ -252,6 +253,7 @@ export function useCodeWalkthroughSteps({
252
253
 
253
254
  const rootTopOffset = root.current?.offsetTop ?? 0;
254
255
  if (!activeStep && rootTopOffset <= options.navbarHeight) {
256
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- seeded from root.current.offsetTop, a post-layout measurement
255
257
  setActiveStep(visibleSteps[0]?.id || null);
256
258
  }
257
259
 
@@ -16,6 +16,7 @@ export function useMenuItemExpanded(item: ItemState): [boolean, Dispatch<SetStat
16
16
  const [controlledValue, setControlledValue] = useState(isItemExpanded(item));
17
17
 
18
18
  useEffect(() => {
19
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- deferred a commit on purpose so CSS transitions still run
19
20
  setIsExpanded(controlledValue);
20
21
  }, [controlledValue]);
21
22
 
@@ -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
- lockObserver.current = true;
254
+ releaseClickScroll();
208
255
  setHeading(headingId);
209
256
 
210
- const headingElement = headingElements.find((el) => el.id === headingId);
211
- if (headingElement && headingElement.scrollIntoView) {
212
- headingElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
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
- setTimeout(() => {
216
- headingElementsRef.current = {};
217
- lockObserver.current = false;
218
- }, 300);
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(() => {
@@ -225,18 +286,22 @@ export function useActiveHeading(
225
286
  return;
226
287
  }
227
288
  const headers = findHeaders(contentElement);
289
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- headings and the initial active heading come from DOM queries and scroll measurement, available only after layout
228
290
  setHeadingElements(headers);
229
291
 
230
292
  if (headers.length > 0 && !heading) {
231
293
  if (isTopOfPage()) {
294
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
232
295
  setHeading(headers[0].id);
233
296
  } else {
234
297
  const totalHeight = scrollY + window.innerHeight;
235
298
  if (totalHeight >= document.body.scrollHeight - 10) {
299
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
236
300
  setHeading(headers[headers.length - 1]?.id || undefined);
237
301
  } else {
238
302
  const triggerOffset = getTriggerOffset();
239
303
  const activeHeader = findActiveHeaderByVirtualPosition(headers, scrollY, triggerOffset);
304
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
240
305
  setHeading(activeHeader.id);
241
306
  }
242
307
  }
@@ -268,7 +333,10 @@ export function useActiveHeading(
268
333
  });
269
334
 
270
335
  const handleScroll = () => {
271
- if (lockObserver.current) return;
336
+ if (clickScroll.current) {
337
+ trackClickScroll();
338
+ return;
339
+ }
272
340
 
273
341
  if (isTopOfPage()) {
274
342
  setHeading(headingElements[0]?.id || undefined);
@@ -281,7 +349,7 @@ export function useActiveHeading(
281
349
  observer.disconnect();
282
350
  window.removeEventListener('scroll', handleScroll);
283
351
  };
284
- }, [headingElements, intersectionCallback, getTriggerOffset]);
352
+ }, [headingElements, intersectionCallback, getTriggerOffset, trackClickScroll]);
285
353
 
286
354
  return { heading, handleHeadingClick };
287
355
  }
@@ -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]);
@@ -6,13 +6,16 @@ import type { MCPOption } from '../types';
6
6
  import { useThemeHooks } from './use-theme-hooks';
7
7
  import { useMCPConfig } from './use-mcp-config';
8
8
  import { ClipboardService } from '../utils/clipboard-service';
9
+ import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
10
+ import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
11
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
9
12
 
10
13
  export type McpButtonOptions = {
11
14
  options?: MCPOption[];
12
15
  };
13
16
 
14
17
  export type McpButtonSettings = {
15
- isCopied: boolean;
18
+ copiedOption: MCPOption | null;
16
19
  cursorUrl: string;
17
20
  vscodeUrl: string;
18
21
  triggerButtonText: string;
@@ -23,42 +26,53 @@ export type McpButtonSettings = {
23
26
  const COPIED_RESET_TIMEOUT = 1000;
24
27
 
25
28
  export function useConnectMCPButton({
26
- options = ['cursor', 'vscode', 'copy'],
29
+ options = DEFAULT_MCP_OPTIONS,
27
30
  }: McpButtonOptions = {}): McpButtonSettings {
28
- const { useTranslate } = useThemeHooks();
31
+ const { useTranslate, useTelemetry } = useThemeHooks();
29
32
  const { translate } = useTranslate();
33
+ const telemetry = useTelemetry();
30
34
  const { serverName, serverUrl, cursorUrl, vscodeUrl } = useMCPConfig();
31
- const [isCopied, setIsCopied] = useState(false);
35
+ const [copiedOption, setCopiedOption] = useState<MCPOption | null>(null);
32
36
 
33
37
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
38
  const resetCopied = useCallback(
35
- debounce(() => setIsCopied(false), COPIED_RESET_TIMEOUT),
39
+ debounce(() => setCopiedOption(null), COPIED_RESET_TIMEOUT),
36
40
  [],
37
41
  );
38
42
 
39
43
  const handleAction = useCallback(
40
44
  (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);
45
+ telemetry.sendConnectMcpClickedMessage([
46
+ { ...getBaseDataAttributes('connectMcp', 'button'), option: action },
47
+ ]);
48
+
49
+ const connection = { serverName, url: serverUrl };
50
+ const copyTextMap: Partial<Record<MCPOption, string>> = {
51
+ copy: JSON.stringify(
52
+ { [serverName]: { url: serverUrl, description: 'MCP Server' } },
53
+ null,
54
+ 2,
55
+ ),
56
+ 'claude-code': generateClaudeCodeCommand(connection),
57
+ codex: generateCodexCommand(connection),
58
+ };
59
+
60
+ const copyText = copyTextMap[action];
61
+ if (copyText !== undefined) {
62
+ ClipboardService.copyCustom(copyText);
63
+ setCopiedOption(action);
50
64
  resetCopied();
51
65
  return;
52
66
  }
53
67
 
54
- const urlMap: Record<Exclude<MCPOption, 'copy'>, string> = {
68
+ const urlMap: Partial<Record<MCPOption, string>> = {
55
69
  cursor: cursorUrl,
56
70
  vscode: vscodeUrl,
57
71
  };
58
72
 
59
73
  window.open(urlMap[action], '_blank');
60
74
  },
61
- [cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied],
75
+ [cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied, telemetry],
62
76
  );
63
77
 
64
78
  const triggerButtonText = useMemo(
@@ -69,7 +83,7 @@ export function useConnectMCPButton({
69
83
  const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
70
84
 
71
85
  return {
72
- isCopied,
86
+ copiedOption,
73
87
  cursorUrl,
74
88
  vscodeUrl,
75
89
  triggerButtonText,
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useRef } from 'react';
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
2
 
3
3
  const DEFAULT_CONTROL_OPEN_DELAY = 300;
4
4
 
@@ -18,6 +18,10 @@ export const useControl = (initialVal = false): UseControlReturnType => {
18
18
  }
19
19
  }, []);
20
20
 
21
+ useEffect(() => {
22
+ return clearOpenTimer;
23
+ }, [clearOpenTimer]);
24
+
21
25
  const handleOpen = useCallback(() => {
22
26
  clearOpenTimer();
23
27
  timeoutRef.current = setTimeout(() => {
@@ -21,6 +21,7 @@ export function useLocalState<T>(key: string, initialValue: T): [T, (value: T) =
21
21
  // then client loads the actual stored value without hydration mismatch
22
22
  useEffect(() => {
23
23
  if (!isBrowser()) return;
24
+ // oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- localStorage is read after mount so SSR and client hydrate from the same value
24
25
  setValue(getStoredValue(key, initialValue));
25
26
  // eslint-disable-next-line react-hooks/exhaustive-deps
26
27
  }, [key]);
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
 
3
3
  import { useThemeHooks } from './use-theme-hooks';
4
4
  import { IS_BROWSER } from '../utils/dom';
5
- import { DEFAULT_MCP_SERVER_NAME } from '../constants';
5
+ import { DEFAULT_MCP_SERVER_NAME, MCP_ROUTE, PUBLIC_MCP_ROUTE } from '../constants';
6
6
  import { generateMCPDeepLink } from '../utils/mcp';
7
7
  import { withPathPrefix } from '../utils/urls';
8
8
 
@@ -13,20 +13,29 @@ export type McpConfig = {
13
13
  cursorUrl: string;
14
14
  vscodeUrl: string;
15
15
  isMcpDisabled: boolean;
16
+ isPublicEndpointPage: boolean;
17
+ alternateServerUrl?: string;
16
18
  };
17
19
 
18
20
  export function useMCPConfig(): McpConfig {
19
- const { useMcpData } = useThemeHooks();
21
+ const { useMcpData, usePageData } = useThemeHooks();
20
22
 
21
23
  const {
22
- docs: { enabled, name },
24
+ docs: { enabled, name, publicEndpoint },
23
25
  } = useMcpData();
26
+ const pageSlug = usePageData()?.slug;
24
27
 
25
28
  const origin = IS_BROWSER
26
29
  ? window.location.origin
27
30
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
28
31
  const serverName = name || DEFAULT_MCP_SERVER_NAME;
29
- const serverUrl = `${origin}${withPathPrefix('/mcp')}`;
32
+ const isPublicEndpointPage = pageSlug === withPathPrefix(PUBLIC_MCP_ROUTE);
33
+ const serverUrl = `${origin}${withPathPrefix(isPublicEndpointPage ? PUBLIC_MCP_ROUTE : MCP_ROUTE)}`;
34
+ const alternateServerUrl = isPublicEndpointPage
35
+ ? `${origin}${withPathPrefix(MCP_ROUTE)}`
36
+ : publicEndpoint
37
+ ? `${origin}${withPathPrefix(PUBLIC_MCP_ROUTE)}`
38
+ : undefined;
30
39
  const isMcpDisabled = !enabled || false;
31
40
 
32
41
  const cursorUrl = useMemo(
@@ -46,5 +55,7 @@ export function useMCPConfig(): McpConfig {
46
55
  cursorUrl,
47
56
  vscodeUrl,
48
57
  isMcpDisabled,
58
+ isPublicEndpointPage,
59
+ alternateServerUrl,
49
60
  };
50
61
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useState } from 'react';
2
2
  import { useLocation } from 'react-router';
3
3
 
4
4
  import { useModalScrollLock } from './use-modal-scroll-lock';
@@ -6,11 +6,19 @@ import { useModalScrollLock } from './use-modal-scroll-lock';
6
6
  export function useMobileMenu(initialState = false) {
7
7
  const location = useLocation();
8
8
  const [isOpen, setIsOpen] = useState(initialState);
9
+ const locationKey = `${location.pathname}${location.hash}`;
10
+ const [prevLocationKey, setPrevLocationKey] = useState(locationKey);
9
11
 
10
- useEffect(() => setIsOpen(false), [location.pathname, location.hash]);
12
+ if (isOpen && prevLocationKey !== locationKey) {
13
+ setPrevLocationKey(locationKey);
14
+ setIsOpen(false);
15
+ }
11
16
  useModalScrollLock(isOpen);
12
17
 
13
- const openMobileMenu = () => setIsOpen(true);
18
+ const openMobileMenu = () => {
19
+ setPrevLocationKey(locationKey);
20
+ setIsOpen(true);
21
+ };
14
22
  const closeMobileMenu = () => setIsOpen(false);
15
23
 
16
24
  return { isOpen, openMobileMenu, closeMobileMenu };
@@ -289,14 +289,16 @@ export const useActiveTab = ({ initialTab, tabsId }: UseActiveTabProps) => {
289
289
  const [activeTab, setActiveTab] = useState<string | undefined>(undefined);
290
290
  const resolvedActiveTab = activeTab ?? initialTab;
291
291
  const prevActiveTabRef = useRef(resolvedActiveTab);
292
+ const isTabResolvedFromHashRef = useRef(false);
292
293
 
293
294
  useEffect(() => {
294
- if (activeTab !== undefined) {
295
+ if (isTabResolvedFromHashRef.current) {
295
296
  return;
296
297
  }
298
+ isTabResolvedFromHashRef.current = true;
297
299
 
298
300
  setActiveTab(getInitialTab({ initialTab, hash: location.hash, tabsId }));
299
- }, [activeTab, initialTab, location.hash, tabsId]);
301
+ }, [initialTab, location.hash, tabsId]);
300
302
 
301
303
  useEffect(() => {
302
304
  const hasActiveTabChanged = prevActiveTabRef.current !== resolvedActiveTab;
@@ -36,6 +36,8 @@ export const useTelemetryFallback = () => ({
36
36
  sendProductPickerChangedMessage: () => {},
37
37
  sendLogoClickedMessage: () => {},
38
38
  sendCodeSnippetReportedMessage: () => {},
39
+ sendConnectMcpClickedMessage: () => {},
40
+ sendMcpConnectionSnippetCopiedMessage: () => {},
39
41
  sendMobileMenuClosedMessage: () => {},
40
42
  sendMobileMenuOpenedMessage: () => {},
41
43
  sendPageViewedMessage: () => {},
@@ -1,5 +1,5 @@
1
1
  // This file is intended to provide selective imports from @redocly/theme/core
2
- // to reduce bundle size by exporting only functionality used in the @redocly/openapi-docs package.
2
+ // to reduce bundle size by exporting only functionality used in the @redocly/api-docs package.
3
3
  //
4
4
  // This selective import approach will help with tree shaking and reduce
5
5
  // the overall bundle size by avoiding importing the entire theme package