@redocly/theme 0.58.0-next.8 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/components/Catalog/CatalogEntity/CatalogEntity.d.ts +5 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntity.js +4 -4
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +3 -3
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.d.ts +5 -1
  5. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +9 -7
  6. package/lib/components/CatalogClassic/CatalogClassic.js +9 -2
  7. package/lib/components/CodeBlock/CodeBlock.d.ts +5 -12
  8. package/lib/components/CodeBlock/CodeBlockControls.d.ts +3 -3
  9. package/lib/components/CodeBlock/CodeBlockControls.js +1 -1
  10. package/lib/components/CodeBlock/CodeBlockDropdown.d.ts +2 -2
  11. package/lib/components/CodeBlock/CodeBlockDropdown.js +4 -13
  12. package/lib/components/CodeBlock/CodeBlockTabs.d.ts +2 -2
  13. package/lib/components/CodeBlock/CodeBlockTabs.js +4 -3
  14. package/lib/components/JsonViewer/JsonViewer.d.ts +1 -1
  15. package/lib/components/JsonViewer/JsonViewer.js +9 -10
  16. package/lib/components/PageActions/PageActions.d.ts +4 -1
  17. package/lib/components/PageActions/PageActions.js +2 -2
  18. package/lib/core/constants/catalog.js +4 -0
  19. package/lib/core/contexts/CodeSnippetContext.d.ts +14 -6
  20. package/lib/core/contexts/CodeSnippetContext.js +57 -14
  21. package/lib/core/hooks/use-codeblock-tabs-controls.d.ts +2 -2
  22. package/lib/core/hooks/use-local-state.js +22 -18
  23. package/lib/core/hooks/use-page-actions.d.ts +2 -1
  24. package/lib/core/hooks/use-page-actions.js +48 -6
  25. package/lib/core/openapi/index.d.ts +1 -0
  26. package/lib/core/openapi/index.js +3 -1
  27. package/lib/core/types/l10n.d.ts +1 -1
  28. package/lib/core/types/open-api-server.d.ts +1 -0
  29. package/lib/icons/CursorIcon/CursorIcon.d.ts +9 -0
  30. package/lib/icons/CursorIcon/CursorIcon.js +22 -0
  31. package/lib/layouts/DocumentationLayout.js +1 -3
  32. package/lib/markdoc/components/CodeGroup/CodeGroup.js +49 -27
  33. package/lib/markdoc/components/Tabs/TabList.js +2 -0
  34. package/package.json +4 -4
  35. package/src/components/Catalog/CatalogEntity/CatalogEntity.tsx +15 -2
  36. package/src/components/Catalog/CatalogEntity/CatalogEntityMetadata.tsx +3 -3
  37. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +27 -18
  38. package/src/components/CatalogClassic/CatalogClassic.tsx +26 -10
  39. package/src/components/CodeBlock/CodeBlock.tsx +5 -11
  40. package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
  41. package/src/components/CodeBlock/CodeBlockDropdown.tsx +11 -20
  42. package/src/components/CodeBlock/CodeBlockTabs.tsx +8 -8
  43. package/src/components/JsonViewer/JsonViewer.tsx +16 -9
  44. package/src/components/PageActions/PageActions.tsx +6 -4
  45. package/src/core/constants/catalog.ts +4 -0
  46. package/src/core/contexts/CodeSnippetContext.tsx +54 -18
  47. package/src/core/hooks/use-codeblock-tabs-controls.ts +2 -2
  48. package/src/core/hooks/use-local-state.ts +28 -19
  49. package/src/core/hooks/use-page-actions.ts +63 -6
  50. package/src/core/openapi/index.ts +1 -0
  51. package/src/core/types/l10n.ts +13 -0
  52. package/src/core/types/open-api-server.ts +1 -0
  53. package/src/icons/CursorIcon/CursorIcon.tsx +35 -0
  54. package/src/layouts/DocumentationLayout.tsx +3 -10
  55. package/src/markdoc/components/CodeGroup/CodeGroup.tsx +81 -52
  56. package/src/markdoc/components/Tabs/TabList.tsx +1 -0
@@ -2,10 +2,7 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
- import type {
6
- CodeBlockDropdownItems,
7
- CodeBlockTabItems,
8
- } from '@redocly/theme/components/CodeBlock/CodeBlock';
5
+ import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
9
6
 
10
7
  import { CodeBlockTabs } from '@redocly/theme/components/CodeBlock/CodeBlockTabs';
11
8
  import { CopyButton } from '@redocly/theme/components/Buttons/CopyButton';
@@ -24,8 +21,8 @@ export type CodeBlockControlsProps = {
24
21
  className?: string;
25
22
  title?: React.ReactNode | string;
26
23
  controls?: ControlItems | false;
27
- tabs?: CodeBlockTabItems;
28
- dropdown?: CodeBlockDropdownItems;
24
+ tabs?: CodeBlockItems;
25
+ dropdown?: CodeBlockItems;
29
26
  };
30
27
 
31
28
  type ControlItems = {
@@ -87,7 +84,7 @@ export function CodeBlockControls({
87
84
  {tabs && <CodeBlockTabs tabs={tabs} />}
88
85
  <ControlsWrapper>
89
86
  {dropdown && <CodeBlockDropdown {...dropdown} />}
90
- {report && !report?.props?.hide ? (
87
+ {report && !report.hidden && !report?.props?.hide ? (
91
88
  <TooltipWrapper
92
89
  tip={translate('codeSnippet.report.tooltipText', 'Report a problem')}
93
90
  placement="top"
@@ -1,24 +1,23 @@
1
1
  import React, { type JSX } from 'react';
2
- import styled from 'styled-components';
3
2
 
4
- import type { CodeBlockDropdownItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
3
+ import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
5
4
 
6
5
  import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
7
6
  import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
8
7
  import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
9
8
  import { Button } from '@redocly/theme/components/Button/Button';
10
- import { ChevronSortIcon } from '@redocly/theme/icons/ChevronSortIcon/ChevronSortIcon';
11
9
  import { NoneIcon } from '@redocly/theme/icons/NoneIcon/NoneIcon';
12
10
  import { getFileIconByLanguage } from '@redocly/theme/core/utils';
13
11
 
14
- export function CodeBlockDropdown({ items, onChange, value }: CodeBlockDropdownItems): JSX.Element {
15
- const activeItem = items.find((item) => item.name === value) || items[0];
12
+ export function CodeBlockDropdown({ items, onChange, value }: CodeBlockItems): JSX.Element {
13
+ const activeItem = items.find((item) => item.id === value) || items[0];
16
14
  const icon = activeItem?.lang ? getFileIconByLanguage(activeItem?.lang) : null;
17
15
  return (
18
- <StyledDropdown
16
+ <Dropdown
17
+ withArrow
19
18
  alignment="end"
20
19
  trigger={
21
- <Button icon={<ChevronSortIcon />} iconPosition="right" variant="text" size="small">
20
+ <Button iconPosition="right" variant="ghost" size="small">
22
21
  {icon}
23
22
  {activeItem.name}
24
23
  </Button>
@@ -27,11 +26,12 @@ export function CodeBlockDropdown({ items, onChange, value }: CodeBlockDropdownI
27
26
  <DropdownMenu>
28
27
  {items.map((item) => {
29
28
  const icon = getFileIconByLanguage(item.lang || '');
29
+ const isActive = item.id === value;
30
30
  return (
31
31
  <DropdownMenuItem
32
- key={item.lang}
33
- onAction={() => onChange(item.name)}
34
- active={item.name === activeItem.name}
32
+ key={item.id}
33
+ onAction={() => onChange(item.id)}
34
+ active={isActive}
35
35
  prefix={item.lang ? icon : <NoneIcon size="var(--icon-size)" />}
36
36
  >
37
37
  {item.name}
@@ -39,15 +39,6 @@ export function CodeBlockDropdown({ items, onChange, value }: CodeBlockDropdownI
39
39
  );
40
40
  })}
41
41
  </DropdownMenu>
42
- </StyledDropdown>
42
+ </Dropdown>
43
43
  );
44
44
  }
45
-
46
- const StyledDropdown = styled(Dropdown)`
47
- margin-left: auto;
48
- --icon-size: 18px;
49
- --button-color: var(--text-color-secondary);
50
- button.button-size-small {
51
- --button-icon-size: 18px;
52
- }
53
- `;
@@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
- import type { CodeBlockTabItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
5
+ import type { CodeBlockItems } from '@redocly/theme/components/CodeBlock/CodeBlock';
6
6
 
7
7
  import { useCodeBlockTabsControls } from '@redocly/theme/core/hooks';
8
8
  import { Button } from '@redocly/theme/components/Button/Button';
@@ -11,7 +11,7 @@ import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronR
11
11
  import { getFileIconByExt, getFileIconByLanguage } from '@redocly/theme/core/utils/get-file-icon';
12
12
 
13
13
  export type CodeBlockTabsProps = {
14
- tabs: CodeBlockTabItems;
14
+ tabs: CodeBlockItems;
15
15
  };
16
16
 
17
17
  export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
@@ -24,7 +24,7 @@ export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
24
24
  });
25
25
 
26
26
  useEffect(() => {
27
- const activeTab = tabRefs.current.find((tab) => tab?.dataset.name === tabs.value);
27
+ const activeTab = tabRefs.current.find((tab) => tab?.dataset.id === tabs.value);
28
28
 
29
29
  if (activeTab) {
30
30
  activeTab.scrollIntoView({ block: 'nearest', inline: 'center' });
@@ -35,23 +35,23 @@ export function CodeBlockTabs({ tabs }: CodeBlockTabsProps): JSX.Element {
35
35
  <CodeBlockTabsWrapper ref={containerRef} data-component-name="CodeBlock/CodeBlockTabs">
36
36
  <ShadowWrapper>
37
37
  <Tabs>
38
- {tabs.items.map(({ name, lang }, i) => {
38
+ {tabs.items.map((item, i) => {
39
+ const { name, lang, id } = item;
39
40
  const ext = name.match(/\.([^.]+)$/)?.[1];
40
41
  const fileIcon = lang
41
42
  ? getFileIconByLanguage(lang)
42
43
  : ext
43
44
  ? getFileIconByExt(ext)
44
45
  : null;
45
-
46
46
  return (
47
47
  <Tab
48
48
  ref={(el: HTMLButtonElement | null) => {
49
49
  tabRefs.current[i] = el as HTMLButtonElement;
50
50
  }}
51
51
  data-name={name}
52
- active={name === tabs.value}
53
- key={name + i}
54
- onClick={() => tabs.onChange(name)}
52
+ active={id === tabs.value}
53
+ key={id}
54
+ onClick={() => tabs.onChange(id)}
55
55
  >
56
56
  {fileIcon}
57
57
  {name}
@@ -12,11 +12,11 @@ export type PanelType = 'request' | 'responses' | 'request-samples' | 'response-
12
12
 
13
13
  export type JsonProps = {
14
14
  title?: CodeBlockControlsProps['title'];
15
+ controls?: CodeBlockControlsProps['controls'];
15
16
  data: any;
16
17
  className?: string;
17
18
  expandLevel: number;
18
19
  startLineNumber?: number;
19
- hideHeader?: boolean;
20
20
  onCopyClick?: () => void;
21
21
  onPanelToggle?: (isExpanded: boolean, panelType?: PanelType) => void;
22
22
  };
@@ -28,7 +28,7 @@ function JsonComponent({
28
28
  onCopyClick,
29
29
  onPanelToggle,
30
30
  title,
31
- hideHeader,
31
+ controls = {},
32
32
  }: JsonProps): JSX.Element {
33
33
  const showFoldingButtons =
34
34
  data && Object.values(data).some((value) => typeof value === 'object' && value !== null);
@@ -53,6 +53,8 @@ function JsonComponent({
53
53
 
54
54
  const source = JSON.stringify(data, null, 2);
55
55
 
56
+ const hasHeader = title || controls;
57
+
56
58
  return (
57
59
  <JsonViewerWrap
58
60
  data-testid="json-viewer"
@@ -61,17 +63,22 @@ function JsonComponent({
61
63
  >
62
64
  <CodeBlock
63
65
  header={
64
- hideHeader
65
- ? undefined
66
- : {
66
+ hasHeader
67
+ ? {
67
68
  title,
68
69
  className: 'code-block-header',
69
- controls: {
70
- copy: { data, onClick: onCopyClick, handleOutside: true },
71
- expand: showFoldingButtons ? { onClick: expandAll } : undefined,
72
- collapse: showFoldingButtons ? { onClick: collapseAll } : undefined,
70
+ controls: controls && {
71
+ ...controls,
72
+ copy: { ...controls.copy, data, onClick: onCopyClick, handleOutside: true },
73
+ expand: showFoldingButtons
74
+ ? { ...controls.expand, onClick: expandAll }
75
+ : undefined,
76
+ collapse: showFoldingButtons
77
+ ? { ...controls.collapse, onClick: collapseAll }
78
+ : undefined,
73
79
  },
74
80
  }
81
+ : undefined
75
82
  }
76
83
  source={source}
77
84
  >
@@ -13,20 +13,22 @@ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
13
13
  import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
14
14
  import { Spinner } from '@redocly/theme/icons/Spinner/Spinner';
15
15
  import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
16
- import { usePageActions } from '@redocly/theme/core/hooks';
16
+ import { PageActionType, usePageActions } from '@redocly/theme/core/hooks';
17
17
 
18
18
  type ActionState = 'idle' | 'processing' | 'done';
19
19
 
20
20
  type PageActionProps = {
21
- pageSlug: string;
21
+ pageSlug?: string;
22
+ mcpUrl?: string;
23
+ actions?: PageActionType[];
22
24
  };
23
25
 
24
26
  const ACTION_DONE_DISPLAY_DURATION = 1000;
25
27
 
26
28
  export function PageActions(props: PageActionProps): JSX.Element | null {
27
- const { pageSlug } = props;
29
+ const { pageSlug, mcpUrl } = props;
28
30
 
29
- const actions = usePageActions(pageSlug || '/');
31
+ const actions = usePageActions(pageSlug || '/', mcpUrl, props.actions);
30
32
 
31
33
  const [actionState, setActionState] = useState<ActionState>('idle');
32
34
 
@@ -38,6 +38,8 @@ export const reverseRelationMap: Record<EntityRelationType, EntityRelationType>
38
38
  memberOf: 'hasMember',
39
39
  triggers: 'triggeredBy',
40
40
  triggeredBy: 'triggers',
41
+ returns: 'returnedBy',
42
+ returnedBy: 'returns',
41
43
  } as const;
42
44
 
43
45
  export const relationTypeMap: Record<EntityRelationType, string> = {
@@ -66,6 +68,8 @@ export const relationTypeMap: Record<EntityRelationType, string> = {
66
68
  memberOf: 'Member of',
67
69
  triggers: 'Triggers',
68
70
  triggeredBy: 'Triggered by',
71
+ returns: 'Returns',
72
+ returnedBy: 'Returned by',
69
73
  };
70
74
 
71
75
  export enum GraphHandleType {
@@ -1,31 +1,67 @@
1
- import { createContext, useContext, useState } from 'react';
1
+ import React, { createContext, useContext, useCallback, useMemo } from 'react';
2
2
 
3
- export type CodeSnippetContextType = {
4
- activeSnippetName: string;
5
- setActiveSnippetName: (name: string) => void;
6
- };
3
+ import { useLocalState } from '../hooks/use-local-state';
7
4
 
8
5
  export const CODE_GROUP_SNIPPET_NAME_KEY = 'redocly:codeGroupSnippetName';
9
6
 
7
+ type CodeSnippetContextType = {
8
+ activeSnippets: Record<string, string>;
9
+ setActiveSnippet: (groupId: string, snippetId: string) => void;
10
+ };
11
+
10
12
  export const CodeSnippetContext = createContext<CodeSnippetContextType | null>(null);
11
13
 
12
- export function useActiveCodeSnippetName(
13
- mode: 'tabs' | 'dropdown',
14
- ): [string, (name: string) => void] {
14
+ export function CodeSnippetProvider({ children }: { children: React.ReactNode }) {
15
+ const [activeSnippets, setActiveSnippets] = useLocalState<Record<string, string>>(
16
+ CODE_GROUP_SNIPPET_NAME_KEY,
17
+ {},
18
+ );
19
+
20
+ const setActiveSnippet = useCallback(
21
+ (groupId: string, snippetId: string) => {
22
+ setActiveSnippets({ ...activeSnippets, [groupId]: snippetId });
23
+ },
24
+ [activeSnippets, setActiveSnippets],
25
+ );
26
+
27
+ const contextValue = { activeSnippets, setActiveSnippet };
28
+
29
+ return <CodeSnippetContext.Provider value={contextValue}>{children}</CodeSnippetContext.Provider>;
30
+ }
31
+
32
+ export const useCodeSnippetContext = (): CodeSnippetContextType => {
15
33
  const context = useContext(CodeSnippetContext);
34
+
16
35
  if (!context) {
17
36
  throw new Error('useCodeSnippetContext must be used within a CodeSnippetContext');
18
37
  }
19
38
 
20
- const [activeSnippetName, setActiveSnippetName] = useState(
21
- mode === 'tabs' ? '' : context.activeSnippetName,
39
+ return context;
40
+ };
41
+
42
+ export const useActiveCodeSnippetId = (
43
+ groupId?: string,
44
+ availableSnippets?: { id: string }[],
45
+ ): [string, (id: string) => void] => {
46
+ const { activeSnippets, setActiveSnippet } = useCodeSnippetContext();
47
+
48
+ const storedSnippetId = groupId ? activeSnippets[groupId] || '' : '';
49
+
50
+ const activeId = useMemo(() => {
51
+ if (!availableSnippets?.length) return storedSnippetId;
52
+
53
+ const found = storedSnippetId && availableSnippets.find((s) => s.id === storedSnippetId);
54
+ return found ? storedSnippetId : availableSnippets[0]?.id || '';
55
+ }, [storedSnippetId, availableSnippets]);
56
+
57
+ const setActiveSnippetId = useCallback(
58
+ (id: string) => {
59
+ if (groupId) {
60
+ setActiveSnippet(groupId, id);
61
+ }
62
+ },
63
+ [groupId, setActiveSnippet],
22
64
  );
23
65
 
24
- if (mode === 'tabs') {
25
- // use non-synced state for tabs mode
26
- return [activeSnippetName, setActiveSnippetName];
27
- } else {
28
- // use global synced state for dropdown mode
29
- return [context.activeSnippetName, context.setActiveSnippetName];
30
- }
31
- }
66
+ return [activeId, setActiveSnippetId];
67
+ };
@@ -1,9 +1,9 @@
1
1
  import { useEffect, useState, useCallback, useMemo } from 'react';
2
2
 
3
- import type { CodeBlockTabItems } from '../../components/CodeBlock/CodeBlock';
3
+ import type { CodeBlockItems } from '../../components/CodeBlock/CodeBlock';
4
4
 
5
5
  type CodeBlockTabsProps = {
6
- tabs: CodeBlockTabItems;
6
+ tabs: CodeBlockItems;
7
7
  containerRef: React.RefObject<HTMLDivElement | null>;
8
8
  tabRefs: React.RefObject<HTMLButtonElement[]>;
9
9
  };
@@ -1,30 +1,39 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ import { isBrowser } from '../utils/js-utils';
4
+
5
+ function getStoredValue<T>(key: string, fallback: T): T {
6
+ if (!isBrowser()) return fallback;
2
7
 
3
- function getInitialValue<T>(key: string, initialValue: T): T {
4
- if (typeof window === 'undefined') {
5
- return initialValue;
6
- }
7
8
  try {
8
9
  const savedValue = localStorage.getItem(key);
9
- if (savedValue) {
10
- return JSON.parse(savedValue) as T;
11
- }
12
- } catch (error) {
13
- console.error(`Error reading from localStorage for key "${key}":`, error);
10
+ return savedValue ? (JSON.parse(savedValue) as T) : fallback;
11
+ } catch {
12
+ return fallback;
14
13
  }
15
- return initialValue;
16
14
  }
17
15
 
18
16
  export function useLocalState<T>(key: string, initialValue: T): [T, (value: T) => void] {
19
- const [value, setValue] = useState<T>(() => getInitialValue(key, initialValue));
17
+ const [value, setValue] = useState<T>(initialValue);
20
18
 
19
+ // Load stored value from localStorage after component mounts
20
+ // This ensures SSR compatibility: server and client both start with initialValue,
21
+ // then client loads the actual stored value without hydration mismatch
21
22
  useEffect(() => {
22
- try {
23
- localStorage.setItem(key, JSON.stringify(value));
24
- } catch (error) {
25
- console.error(`Error writing to localStorage for key "${key}":`, error);
26
- }
27
- }, [key, value]);
23
+ if (!isBrowser()) return;
24
+ setValue(getStoredValue(key, initialValue));
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, [key]);
27
+
28
+ const handleSetValue = useCallback(
29
+ (newValue: T) => {
30
+ setValue(newValue);
31
+ if (isBrowser()) {
32
+ localStorage.setItem(key, JSON.stringify(newValue));
33
+ }
34
+ },
35
+ [key],
36
+ );
28
37
 
29
- return [value, setValue] as const;
38
+ return [value, handleSetValue] as const;
30
39
  }
@@ -12,10 +12,19 @@ import { useThemeHooks } from './use-theme-hooks';
12
12
  import { useThemeConfig } from './use-theme-config';
13
13
  import { ClipboardService } from '../utils/clipboard-service';
14
14
  import { IS_BROWSER } from '../utils/dom';
15
+ import { CursorIcon } from '../../icons/CursorIcon/CursorIcon';
15
16
 
16
17
  const DEFAULT_ENABLED_ACTIONS = ['copy', 'view', 'chatgpt', 'claude'] as const;
18
+ const CURSOR_URL =
19
+ 'cursor://anysphere.cursor-deeplink/mcp/install?name=$NAME&config=$BASE64_ENCODED_CONFIG';
17
20
 
18
- export function usePageActions(pageSlug: string): PageAction[] {
21
+ export type PageActionType = 'copy' | 'view' | 'chatgpt' | 'claude' | 'mcp-cursor';
22
+
23
+ export function usePageActions(
24
+ pageSlug: string,
25
+ mcpUrl?: string,
26
+ actions?: PageActionType[],
27
+ ): PageAction[] {
19
28
  const { useTranslate, usePageData, usePageProps, usePageSharedData } = useThemeHooks();
20
29
  const { translate } = useTranslate();
21
30
 
@@ -32,7 +41,7 @@ export function usePageActions(pageSlug: string): PageAction[] {
32
41
  );
33
42
  const { isPublic } = usePageData() || {};
34
43
 
35
- const actions: PageAction[] = useMemo(() => {
44
+ const result: PageAction[] = useMemo(() => {
36
45
  if (shouldHideAllActions) {
37
46
  return [];
38
47
  }
@@ -54,9 +63,49 @@ export function usePageActions(pageSlug: string): PageAction[] {
54
63
  return url.toString();
55
64
  }
56
65
 
57
- return (themeConfig.navigation?.actions?.items || DEFAULT_ENABLED_ACTIONS)
66
+ return (themeConfig.navigation?.actions?.items || actions || DEFAULT_ENABLED_ACTIONS)
58
67
  .map((action) => {
68
+ function generateMCPConfig(isCursor?: boolean): string {
69
+ const jsonConfig = {
70
+ 'mcp-server': {
71
+ url: mcpUrl,
72
+ description: 'MCP Server',
73
+ },
74
+ };
75
+ if (isCursor) {
76
+ const url = CURSOR_URL.replace('$NAME', 'mcp-server').replace(
77
+ '$BASE64_ENCODED_CONFIG',
78
+ btoa(JSON.stringify(jsonConfig['mcp-server'])),
79
+ );
80
+ return url;
81
+ }
82
+
83
+ return JSON.stringify(jsonConfig, null, 2);
84
+ }
85
+
59
86
  switch (action) {
87
+ case 'mcp-cursor':
88
+ if (!mcpUrl) {
89
+ return null;
90
+ }
91
+
92
+ return {
93
+ buttonText: translate('page.actions.cursorMcpButtonText', 'Connect to Cursor'),
94
+ title: translate('page.actions.cursorMcpTitle', 'Connect to Cursor'),
95
+ description: translate(
96
+ 'page.actions.cursorMcpDescription',
97
+ 'Install MCP server on Cursor',
98
+ ),
99
+ iconComponent: CursorIcon,
100
+ onClick: () => {
101
+ try {
102
+ const url = generateMCPConfig(true);
103
+ window.open(url, '_blank');
104
+ } catch (error) {
105
+ console.error(error);
106
+ }
107
+ },
108
+ };
60
109
  case 'copy':
61
110
  return {
62
111
  buttonText: translate('page.actions.copyButtonText', 'Copy'),
@@ -121,9 +170,17 @@ export function usePageActions(pageSlug: string): PageAction[] {
121
170
  }
122
171
  })
123
172
  .filter((action) => action !== null);
124
- }, [themeConfig, translate, pageSlug, isPublic, shouldHideAllActions]);
125
-
126
- return actions;
173
+ }, [
174
+ shouldHideAllActions,
175
+ pageSlug,
176
+ themeConfig.navigation?.actions?.items,
177
+ actions,
178
+ mcpUrl,
179
+ translate,
180
+ isPublic,
181
+ ]);
182
+
183
+ return result;
127
184
  }
128
185
 
129
186
  function shouldHidePageActions(
@@ -17,6 +17,7 @@ export {
17
17
  addTrailingSlash,
18
18
  withPathPrefix,
19
19
  } from '../utils/urls';
20
+ export { capitalize } from '../utils/string';
20
21
  export { typedMemo } from '../hoc/typedMemo';
21
22
  export { useMount } from '../hooks/use-mount';
22
23
  export { GlobalStyle } from '../styles/global';
@@ -206,6 +206,9 @@ export type TranslationKey =
206
206
  | 'page.actions.claudeTitle'
207
207
  | 'page.actions.claudeButtonText'
208
208
  | 'page.actions.claudeDescription'
209
+ | 'page.actions.cursorMcpButtonText'
210
+ | 'page.actions.cursorMcpTitle'
211
+ | 'page.actions.cursorMcpDescription'
209
212
  | 'openapi.download.description.title'
210
213
  | 'openapi.info.title'
211
214
  | 'openapi.info.contact.url'
@@ -283,6 +286,16 @@ export type TranslationKey =
283
286
  | 'openapi.schemaCatalogLink.title'
284
287
  | 'openapi.schemaCatalogLink.copyButtonTooltip'
285
288
  | 'openapi.schemaCatalogLink.copiedTooltip'
289
+ | 'openapi.mcp.title'
290
+ | 'openapi.mcp.endpoint'
291
+ | 'openapi.mcp.tools'
292
+ | 'openapi.mcp.protocolVersion'
293
+ | 'openapi.mcp.capabilities'
294
+ | 'openapi.mcp.experimentalCapabilities'
295
+ | 'openapi.mcp.inputSchema'
296
+ | 'openapi.mcp.inputExample'
297
+ | 'openapi.mcp.outputSchema'
298
+ | 'openapi.mcp.outputExample'
286
299
  | 'asyncapi.download.description.title'
287
300
  | 'asyncapi.info.title'
288
301
  | 'graphql.queries'
@@ -1,6 +1,7 @@
1
1
  export type OpenAPIServer = {
2
2
  url: string;
3
3
  description?: string;
4
+ name?: string;
4
5
  variables?: Record<
5
6
  string,
6
7
  {
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ const Icon = (props: IconProps) => (
7
+ <svg
8
+ width="16"
9
+ height="16"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ {...props}
14
+ >
15
+ <path d="M7.99956 15V8.0001L2 11.4999L7.99956 15Z" fill="#939393" />
16
+ <path d="M14 4.49979L7.99956 15V8.0001L14 4.49979Z" fill="#E3E3E3" />
17
+ <path d="M2 4.49979H14L7.99956 8.0001L2 4.49979Z" fill="white" />
18
+ <path d="M8.00025 1V4.49995L14 4.49979L8.00025 1Z" fill="#444444" />
19
+ <path
20
+ d="M2 4.49979L8.00025 4.49995V1L2 4.49979ZM13.9999 11.4998L10.9999 9.74987L7.99956 15L13.9999 11.4998Z"
21
+ fill="#939393"
22
+ />
23
+ <path
24
+ d="M14 4.49979L10.9999 9.74987L13.9999 11.4998L14 4.49979ZM7.99956 8.0001L2 11.4999V4.49979L7.99956 8.0001Z"
25
+ fill="#444444"
26
+ />
27
+ </svg>
28
+ );
29
+
30
+ export const CursorIcon = styled(Icon).attrs(() => ({
31
+ 'data-component-name': 'icons/CursorIcon/CursorIcon',
32
+ }))<IconProps>`
33
+ height: ${({ size }) => size || '16px'};
34
+ width: ${({ size }) => size || '16px'};
35
+ `;
@@ -9,12 +9,7 @@ import { breakpoints } from '@redocly/theme/core/utils';
9
9
  import { PageNavigation } from '@redocly/theme/components/PageNavigation/PageNavigation';
10
10
  import { LastUpdated } from '@redocly/theme/components/LastUpdated/LastUpdated';
11
11
  import { Breadcrumbs as ThemeBreadcrumbs } from '@redocly/theme/components/Breadcrumbs/Breadcrumbs';
12
-
13
- import {
14
- CodeSnippetContext,
15
- CODE_GROUP_SNIPPET_NAME_KEY,
16
- } from '../core/contexts/CodeSnippetContext';
17
- import { useLocalState } from '../core/hooks/use-local-state';
12
+ import { CodeSnippetProvider } from '@redocly/theme/core/contexts/CodeSnippetContext';
18
13
 
19
14
  type DocumentationLayoutProps = {
20
15
  tableOfContent: React.ReactNode;
@@ -44,10 +39,8 @@ export function DocumentationLayout({
44
39
  const { editPage: themeEditPage } = config || {};
45
40
  const mergedConf = editPage ? { ...themeEditPage, ...editPage } : undefined;
46
41
 
47
- const [activeSnippetName, setActiveSnippetName] = useLocalState(CODE_GROUP_SNIPPET_NAME_KEY, '');
48
-
49
42
  return (
50
- <CodeSnippetContext.Provider value={{ activeSnippetName, setActiveSnippetName }}>
43
+ <CodeSnippetProvider>
51
44
  <LayoutWrapper data-component-name="Layout/DocumentationLayout" className={className}>
52
45
  <ContentWrapper withToc={!config?.toc?.hide}>
53
46
  <Breadcrumbs />
@@ -61,7 +54,7 @@ export function DocumentationLayout({
61
54
  </ContentWrapper>
62
55
  {tableOfContent}
63
56
  </LayoutWrapper>
64
- </CodeSnippetContext.Provider>
57
+ </CodeSnippetProvider>
65
58
  );
66
59
  }
67
60