@redocly/theme 0.67.0 → 0.68.0-next.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.
@@ -5,4 +5,4 @@ export type ConnectMCPButtonProps = {
5
5
  alignment?: 'start' | 'end';
6
6
  options?: MCPOption[];
7
7
  };
8
- export declare function ConnectMCPButton({ placement, alignment, options }: ConnectMCPButtonProps): JSX.Element;
8
+ export declare function ConnectMCPButton({ placement, alignment, options }: ConnectMCPButtonProps): JSX.Element | null;
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ConnectMCPButton = ConnectMCPButton;
40
40
  const react_1 = __importStar(require("react"));
41
41
  const styled_components_1 = __importDefault(require("styled-components"));
42
- const Dropdown_1 = require("../../components/Dropdown/Dropdown");
43
42
  const DropdownMenu_1 = require("../../components/Dropdown/DropdownMenu");
44
43
  const DropdownMenuItem_1 = require("../../components/Dropdown/DropdownMenuItem");
45
44
  const Button_1 = require("../../components/Button/Button");
@@ -47,12 +46,9 @@ const CursorIcon_1 = require("../../icons/CursorIcon/CursorIcon");
47
46
  const VSCodeIcon_1 = require("../../icons/VSCodeIcon/VSCodeIcon");
48
47
  const CopyIcon_1 = require("../../icons/CopyIcon/CopyIcon");
49
48
  const CheckmarkFilledIcon_1 = require("../../icons/CheckmarkFilledIcon/CheckmarkFilledIcon");
50
- const ConnectIcon_1 = require("../../icons/ConnectIcon/ConnectIcon");
49
+ const ChevronDownIcon_1 = require("../../icons/ChevronDownIcon/ChevronDownIcon");
50
+ const ChevronUpIcon_1 = require("../../icons/ChevronUpIcon/ChevronUpIcon");
51
51
  const hooks_1 = require("../../core/hooks");
52
- const hooks_2 = require("../../core/hooks");
53
- const TriggerButton = react_1.default.memo(({ text }) => {
54
- return (react_1.default.createElement(StyledButton, { variant: "secondary", icon: react_1.default.createElement(ConnectIcon_1.ConnectIcon, null) }, text));
55
- });
56
52
  const MENU_OPTIONS = [
57
53
  {
58
54
  key: 'cursor',
@@ -80,31 +76,68 @@ const MENU_OPTIONS = [
80
76
  },
81
77
  ];
82
78
  function ConnectMCPButton({ placement = 'bottom', alignment = 'end', options = ['cursor', 'vscode', 'copy'], }) {
83
- const { useTranslate } = (0, hooks_2.useThemeHooks)();
79
+ const { useTranslate } = (0, hooks_1.useThemeHooks)();
84
80
  const { translate } = useTranslate();
85
- const { isCopied, triggerButtonText, visibleOptions, handleAction } = (0, hooks_1.useConnectMCPButton)({
86
- options,
87
- });
88
- const menuOptions = (0, react_1.useMemo)(() => MENU_OPTIONS.filter((option) => visibleOptions.includes(option.key)), [visibleOptions]);
89
- return (react_1.default.createElement(ConnectMCPButtonWrapper, { "data-component-name": "Buttons/ConnectMCPButton" },
90
- react_1.default.createElement(Dropdown_1.Dropdown, { trigger: react_1.default.createElement(TriggerButton, { text: triggerButtonText }), triggerEvent: "hover", placement: placement, alignment: alignment, closeOnClick: false },
91
- react_1.default.createElement(DropdownMenu_1.DropdownMenu, null, menuOptions.map((option) => {
81
+ const [isOpen, setIsOpen] = (0, react_1.useState)(false);
82
+ const wrapperRef = (0, react_1.useRef)(null);
83
+ const { isCopied, visibleOptions, handleAction } = (0, hooks_1.useConnectMCPButton)({ options });
84
+ (0, hooks_1.useOutsideClick)(wrapperRef, () => setIsOpen(false));
85
+ const menuOptions = MENU_OPTIONS.filter((option) => visibleOptions.includes(option.key));
86
+ const primaryOption = menuOptions[0];
87
+ const dropdownOptions = menuOptions.slice(1);
88
+ if (!primaryOption)
89
+ return null;
90
+ const PrimaryIcon = primaryOption.icon;
91
+ // When copy is the primary action there is no dropdown item to show the copied state, so the
92
+ // primary button itself must reflect it.
93
+ const showPrimaryCopied = primaryOption.key === 'copy' && isCopied;
94
+ const handleMenuAction = (key) => {
95
+ handleAction(key);
96
+ setIsOpen(false);
97
+ };
98
+ return (react_1.default.createElement(ConnectMCPButtonWrapper, { ref: wrapperRef, "data-component-name": "Buttons/ConnectMCPButton" },
99
+ react_1.default.createElement(SplitButtonGroup, null,
100
+ react_1.default.createElement(PrimaryButton, { variant: "secondary", icon: showPrimaryCopied ? (react_1.default.createElement(CheckmarkFilledIcon_1.CheckmarkFilledIcon, { color: "var(--color-success-base)" })) : (react_1.default.createElement(PrimaryIcon, null)), onClick: () => handleAction(primaryOption.key) }, translate(primaryOption.titleTranslationKey, primaryOption.titleDefault)),
101
+ dropdownOptions.length > 0 && (react_1.default.createElement(ChevronButton, { variant: "secondary", "aria-label": translate('page.actions.connectMcp.moreOptions', 'More options'), "aria-expanded": isOpen, "aria-haspopup": "menu", onClick: () => setIsOpen((prev) => !prev) }, isOpen ? react_1.default.createElement(ChevronUpIcon_1.ChevronUpIcon, null) : react_1.default.createElement(ChevronDownIcon_1.ChevronDownIcon, null)))),
102
+ isOpen && dropdownOptions.length > 0 && (react_1.default.createElement(DropdownWrapper, { $placement: placement, $alignment: alignment },
103
+ react_1.default.createElement(DropdownMenu_1.DropdownMenu, null, dropdownOptions.map((option) => {
92
104
  const Icon = option.icon;
93
105
  const showCheckmark = option.key === 'copy' && isCopied;
94
- return (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { key: option.key, onAction: () => handleAction(option.key) },
106
+ return (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { key: option.key, onAction: () => handleMenuAction(option.key) },
95
107
  react_1.default.createElement(MenuItemContent, null,
96
108
  react_1.default.createElement(MenuItemIcon, null, showCheckmark ? (react_1.default.createElement(CheckmarkFilledIcon_1.CheckmarkFilledIcon, { color: "var(--color-success-base)" })) : (react_1.default.createElement(Icon, null))),
97
109
  react_1.default.createElement(MenuItemText, null,
98
110
  react_1.default.createElement(MenuItemTitle, null, translate(option.titleTranslationKey, option.titleDefault)),
99
111
  react_1.default.createElement(MenuItemDescription, null, translate(option.descriptionTranslationKey, option.descriptionDefault))))));
100
- })))));
112
+ }))))));
101
113
  }
102
114
  const ConnectMCPButtonWrapper = styled_components_1.default.div `
103
115
  display: inline-block;
104
116
  position: relative;
105
117
  `;
106
- const StyledButton = (0, styled_components_1.default)(Button_1.Button) `
118
+ const SplitButtonGroup = styled_components_1.default.div `
119
+ display: inline-flex;
120
+ align-items: stretch;
121
+ `;
122
+ const PrimaryButton = (0, styled_components_1.default)(Button_1.Button) `
107
123
  --button-gap: var(--connect-mcp-button-gap);
124
+ border-top-right-radius: 0;
125
+ border-bottom-right-radius: 0;
126
+ `;
127
+ const ChevronButton = (0, styled_components_1.default)(Button_1.Button) `
128
+ border-top-left-radius: 0;
129
+ border-bottom-left-radius: 0;
130
+ border-left: none;
131
+ padding-inline: var(--connect-mcp-button-chevron-padding-inline);
132
+ `;
133
+ const DropdownWrapper = styled_components_1.default.div `
134
+ position: absolute;
135
+ top: ${({ $placement }) => ($placement === 'top' ? 'auto' : '100%')};
136
+ bottom: ${({ $placement }) => ($placement === 'top' ? '100%' : 'auto')};
137
+ left: ${({ $alignment }) => ($alignment === 'start' ? '0' : 'auto')};
138
+ right: ${({ $alignment }) => ($alignment === 'end' ? '0' : 'auto')};
139
+ padding-top: var(--dropdown-menu-padding-top);
140
+ z-index: var(--dropdown-menu-z-index);
108
141
  `;
109
142
  const MenuItemContent = styled_components_1.default.div `
110
143
  display: flex;
@@ -59,6 +59,9 @@ exports.connectMCPButton = (0, styled_components_1.css) `
59
59
  /* Button gap */
60
60
  --connect-mcp-button-gap: var(--spacing-xs);
61
61
 
62
+ /* Chevron button */
63
+ --connect-mcp-button-chevron-padding-inline: var(--spacing-xs);
64
+
62
65
  /* Menu item layout */
63
66
  --connect-mcp-button-menu-item-gap: var(--spacing-sm);
64
67
  --connect-mcp-button-menu-item-padding-block: var(--spacing-xxs);
@@ -41,14 +41,13 @@ const React = __importStar(require("react"));
41
41
  const styled_components_1 = __importDefault(require("styled-components"));
42
42
  const hooks_1 = require("../../core/hooks");
43
43
  function Reasons({ settings, onChange, className }) {
44
- const { label, component, items = [] } = settings;
44
+ const { label, component = 'checkbox', items = [] } = settings;
45
45
  const [checkedState, setCheckedState] = React.useState(new Array(items.length).fill(false));
46
46
  const { useTranslate } = (0, hooks_1.useThemeHooks)();
47
47
  const { translate } = useTranslate();
48
48
  if (!items.length) {
49
49
  return null;
50
50
  }
51
- const input_type = component || 'checkbox';
52
51
  const handleOptionChange = (position) => {
53
52
  const updatedCheckedState = component === 'checkbox'
54
53
  ? checkedState.map((item, index) => (index === position ? !item : item))
@@ -60,7 +59,7 @@ function Reasons({ settings, onChange, className }) {
60
59
  React.createElement(Label, null, label ||
61
60
  translate('feedback.settings.reasons.label', 'Which statement describes your thoughts about this page?')),
62
61
  items.map((reason, idx) => (React.createElement(OptionWrapper, { key: reason },
63
- React.createElement("input", { type: input_type, value: reason, checked: checkedState[idx], name: "reasons", onChange: () => handleOptionChange(idx) }),
62
+ React.createElement("input", { type: component, value: reason, checked: checkedState[idx], name: "reasons", onChange: () => handleOptionChange(idx) }),
64
63
  React.createElement("label", { "data-translation-key": `feedback.settings.reasons.items.${idx + 1}`, id: reason, onClick: () => handleOptionChange(idx) }, translate(`feedback.settings.reasons.items.${idx + 1}`, reason)))))));
65
64
  }
66
65
  const ReasonsWrapper = styled_components_1.default.div `
@@ -49,6 +49,7 @@ const GenericIcon_1 = require("../../icons/GenericIcon/GenericIcon");
49
49
  const Tag_1 = require("../../components/Tag/Tag");
50
50
  function MenuItem(props) {
51
51
  var _a, _b;
52
+ var _c;
52
53
  const { item, depth, className, onClick } = props;
53
54
  const { useTranslate, useTelemetry } = (0, hooks_1.useThemeHooks)();
54
55
  const { translate } = useTranslate();
@@ -101,7 +102,7 @@ function MenuItem(props) {
101
102
  return (react_1.default.createElement(MenuItemWrapper, { "data-component-name": "Menu/MenuItem", className: generateClassName({ type, item, className }), role: "listitem" },
102
103
  item.link ? (react_1.default.createElement(MenuItemLink, { to: item.link, external: item.external, target: item.target, languageInsensitive: item.languageInsensitive, onKeyDown: handleExpandOnEnter }, label)) : (label),
103
104
  isNested ? (react_1.default.createElement(MenuItemNestedWrapper, { $isExpanded: isExpanded, $depth: depth, ref: nestedMenuRef, style: style }, isExpanded || !canUnmount ? props.children : null)) : null,
104
- item.separatorLine ? (react_1.default.createElement(MenuItemSeparatorLine, { $depth: depth, $linePosition: item.linePosition })) : null));
105
+ item.separatorLine ? (react_1.default.createElement(MenuItemSeparatorLine, { $depth: depth, $linePosition: (_c = item.linePosition) !== null && _c !== void 0 ? _c : constants_1.DEFAULT_LINE_POSITION })) : null));
105
106
  }
106
107
  /* for backward compatibility */
107
108
  function isDirectColorValue(color) {
@@ -47,11 +47,12 @@ const SearchAiMessage_1 = require("../../components/Search/SearchAiMessage");
47
47
  const Admonition_1 = require("../../components/Admonition/Admonition");
48
48
  const AiStarsIcon_1 = require("../../icons/AiStarsIcon/AiStarsIcon");
49
49
  function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error, resources, onMessageSent, className, conversation, setConversation, toolCalls = [], contentSegments, }) {
50
+ var _a;
50
51
  const { useTranslate } = (0, hooks_1.useThemeHooks)();
51
52
  const { aiAssistant } = (0, hooks_1.useThemeConfig)();
52
53
  const { translate } = useTranslate();
53
54
  const conversationEndRef = react_1.default.useRef(null);
54
- const suggestions = aiAssistant === null || aiAssistant === void 0 ? void 0 : aiAssistant.suggestions;
55
+ const suggestions = (_a = aiAssistant === null || aiAssistant === void 0 ? void 0 : aiAssistant.suggestions) !== null && _a !== void 0 ? _a : [];
55
56
  const placeholder = isGeneratingResponse
56
57
  ? translate('search.ai.generatingResponse', 'Generating response...')
57
58
  : conversation.length > 0
@@ -19,6 +19,7 @@ const ErrorFilledIcon_1 = require("../../icons/ErrorFilledIcon/ErrorFilledIcon")
19
19
  const constants_1 = require("../../core/constants");
20
20
  const ChatIcon_1 = require("../../icons/ChatIcon/ChatIcon");
21
21
  function SearchAiResponse(props) {
22
+ var _a;
22
23
  const { useMarkdownText } = (0, hooks_1.useThemeHooks)();
23
24
  const { question, response, isGeneratingResponse, resources, error, onSuggestionClick } = props;
24
25
  const { aiAssistant } = (0, hooks_1.useThemeConfig)();
@@ -26,7 +27,7 @@ function SearchAiResponse(props) {
26
27
  const { translate } = useTranslate();
27
28
  const markdownResponse = useMarkdownText(response || '');
28
29
  let responseContainer = null;
29
- const suggestions = aiAssistant === null || aiAssistant === void 0 ? void 0 : aiAssistant.suggestions;
30
+ const suggestions = (_a = aiAssistant === null || aiAssistant === void 0 ? void 0 : aiAssistant.suggestions) !== null && _a !== void 0 ? _a : [];
30
31
  const hasPendingOrReceivedResponse = response || isGeneratingResponse || error;
31
32
  if (hasPendingOrReceivedResponse) {
32
33
  let icon;
@@ -43,14 +43,14 @@ const hooks_1 = require("../../core/hooks");
43
43
  const Button_1 = require("../../components/Button/Button");
44
44
  const SearchIcon_1 = require("../../icons/SearchIcon/SearchIcon");
45
45
  const utils_1 = require("../../core/utils");
46
+ const constants_1 = require("../../core/constants");
46
47
  function SearchTrigger({ onClick, className }) {
47
48
  var _a;
48
49
  var _b;
49
50
  const themeSettings = (0, hooks_1.useThemeConfig)();
50
51
  const { useTranslate } = (0, hooks_1.useThemeHooks)();
51
52
  const { translate } = useTranslate();
52
- const defaultKeyShortcut = '⌘+K,CTRL+K';
53
- const keyShortcuts = (_b = (_a = themeSettings === null || themeSettings === void 0 ? void 0 : themeSettings.search) === null || _a === void 0 ? void 0 : _a.shortcuts) !== null && _b !== void 0 ? _b : [defaultKeyShortcut];
53
+ const keyShortcuts = (_b = (_a = themeSettings === null || themeSettings === void 0 ? void 0 : themeSettings.search) === null || _a === void 0 ? void 0 : _a.shortcuts) !== null && _b !== void 0 ? _b : [constants_1.DEFAULT_KEYBOARD_SHORTCUTS];
54
54
  let mainShortcutKey = null;
55
55
  if (keyShortcuts) {
56
56
  if (Array.isArray(keyShortcuts)) {
@@ -67,7 +67,7 @@ function SearchTrigger({ onClick, className }) {
67
67
  React.createElement(SearchTriggerInput, { "data-testid": "search-trigger-input", "data-translation-key": "search.navbar.label" },
68
68
  React.createElement(SearchIcon_1.SearchIcon, null),
69
69
  translate('search.navbar.label', 'Search'),
70
- mainShortcutKey === defaultKeyShortcut ? (React.createElement("span", null, isMac ? '⌘K' : 'Ctrl+K')) : (React.createElement("span", null, mainShortcutKey)))));
70
+ mainShortcutKey === constants_1.DEFAULT_KEYBOARD_SHORTCUTS ? (React.createElement("span", null, isMac ? '⌘K' : 'Ctrl+K')) : (React.createElement("span", null, mainShortcutKey)))));
71
71
  }
72
72
  const SearchTriggerWrapper = styled_components_1.default.div `
73
73
  color: var(--search-trigger-color);
@@ -30,3 +30,4 @@ export declare const DEFAULT_COLOR_MODES: {
30
30
  readonly LIGHT: 'light';
31
31
  readonly DARK: 'dark';
32
32
  };
33
+ export declare const DEFAULT_LINE_POSITION = "top";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_COLOR_MODES = exports.MobileMenuType = exports.MenuItemType = exports.DEFAULT_FEEDBACK_TYPE = exports.FEEDBACK_TYPES = exports.ICONS_CDN_URL = exports.CONTENT_ID = exports.DEFAULT_LOCALE_PLACEHOLDER = exports.REDOCLY_TEAMS_RBAC = void 0;
3
+ exports.DEFAULT_LINE_POSITION = exports.DEFAULT_COLOR_MODES = exports.MobileMenuType = exports.MenuItemType = exports.DEFAULT_FEEDBACK_TYPE = exports.FEEDBACK_TYPES = exports.ICONS_CDN_URL = exports.CONTENT_ID = exports.DEFAULT_LOCALE_PLACEHOLDER = exports.REDOCLY_TEAMS_RBAC = void 0;
4
4
  var config_1 = require("@redocly/config");
5
5
  Object.defineProperty(exports, "REDOCLY_TEAMS_RBAC", { enumerable: true, get: function () { return config_1.REDOCLY_TEAMS_RBAC; } });
6
6
  exports.DEFAULT_LOCALE_PLACEHOLDER = 'default_locale';
@@ -37,4 +37,5 @@ exports.DEFAULT_COLOR_MODES = {
37
37
  LIGHT: 'light',
38
38
  DARK: 'dark',
39
39
  };
40
+ exports.DEFAULT_LINE_POSITION = 'top';
40
41
  //# sourceMappingURL=common.js.map
@@ -14,6 +14,7 @@ export type AiSearchConversationRole = (typeof AiSearchConversationRole)[keyof t
14
14
  export declare const AI_SEARCH_ERROR_CONFIG: Record<AiSearchError, AiSearchErrorConfig>;
15
15
  export declare const AI_SEARCH_MAX_MESSAGE_LENGTH = 45000;
16
16
  export declare const SEARCH_DEBOUNCE_TIME_MS = 300;
17
+ export declare const DEFAULT_KEYBOARD_SHORTCUTS = "\u2318+K,CTRL+K";
17
18
  export declare const TOOL_CALL_DISPLAY_TEXT: Partial<Record<string, {
18
19
  inProgressText: string;
19
20
  completedText: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TOOL_CALL_DISPLAY_TEXT = exports.SEARCH_DEBOUNCE_TIME_MS = exports.AI_SEARCH_MAX_MESSAGE_LENGTH = exports.AI_SEARCH_ERROR_CONFIG = exports.AiSearchConversationRole = exports.AiSearchError = void 0;
3
+ exports.TOOL_CALL_DISPLAY_TEXT = exports.DEFAULT_KEYBOARD_SHORTCUTS = exports.SEARCH_DEBOUNCE_TIME_MS = exports.AI_SEARCH_MAX_MESSAGE_LENGTH = exports.AI_SEARCH_ERROR_CONFIG = exports.AiSearchConversationRole = exports.AiSearchError = void 0;
4
4
  const search_1 = require("../types/search");
5
5
  var AiSearchError;
6
6
  (function (AiSearchError) {
@@ -39,6 +39,7 @@ exports.AI_SEARCH_ERROR_CONFIG = {
39
39
  };
40
40
  exports.AI_SEARCH_MAX_MESSAGE_LENGTH = 45000;
41
41
  exports.SEARCH_DEBOUNCE_TIME_MS = 300;
42
+ exports.DEFAULT_KEYBOARD_SHORTCUTS = '⌘+K,CTRL+K';
42
43
  exports.TOOL_CALL_DISPLAY_TEXT = {
43
44
  [search_1.ToolCallName.SearchDocumentation]: {
44
45
  inProgressText: 'Searching documentation...',
@@ -11,6 +11,7 @@ const use_theme_hooks_1 = require("../use-theme-hooks");
11
11
  const use_theme_config_1 = require("../use-theme-config");
12
12
  const contexts_1 = require("../../contexts");
13
13
  const utils_1 = require("../../utils");
14
+ const constants_1 = require("../../constants");
14
15
  function useSearchDialog() {
15
16
  var _a;
16
17
  var _b;
@@ -20,7 +21,7 @@ function useSearchDialog() {
20
21
  const { useTelemetry } = (0, use_theme_hooks_1.useThemeHooks)();
21
22
  const telemetry = useTelemetry();
22
23
  const { refreshSearchSessionId } = (0, contexts_1.useSearchSession)();
23
- const keyShortcuts = (_b = (_a = themeSettings === null || themeSettings === void 0 ? void 0 : themeSettings.search) === null || _a === void 0 ? void 0 : _a.shortcuts) !== null && _b !== void 0 ? _b : ['⌘+K,CTRL+K'];
24
+ const keyShortcuts = (_b = (_a = themeSettings === null || themeSettings === void 0 ? void 0 : themeSettings.search) === null || _a === void 0 ? void 0 : _a.shortcuts) !== null && _b !== void 0 ? _b : [constants_1.DEFAULT_KEYBOARD_SHORTCUTS];
24
25
  const hotKeys = keyShortcuts === null || keyShortcuts === void 0 ? void 0 : keyShortcuts.join(',');
25
26
  (0, react_1.useEffect)(() => {
26
27
  if (hotKeys) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.67.0",
3
+ "version": "0.68.0-next.0",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -63,7 +63,7 @@
63
63
  "vitest": "4.1.8",
64
64
  "vitest-when": "0.6.2",
65
65
  "webpack": "5.105.2",
66
- "@redocly/realm-asyncapi-sdk": "0.13.0"
66
+ "@redocly/realm-asyncapi-sdk": "0.14.0-next.0"
67
67
  },
68
68
  "dependencies": {
69
69
  "@tanstack/react-query": "5.62.3",
@@ -81,7 +81,7 @@
81
81
  "openapi-sampler": "^1.7.2",
82
82
  "react-calendar": "5.1.0",
83
83
  "react-date-picker": "11.0.0",
84
- "@redocly/config": "0.50.1"
84
+ "@redocly/config": "0.52.0"
85
85
  },
86
86
  "scripts": {
87
87
  "watch": "tsgo -p tsconfig.build.json && (concurrently \"tsgo -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\")",
@@ -1,10 +1,9 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useState, useRef } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
  import type { MCPOption } from '@redocly/theme/core/types';
6
6
 
7
- import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
8
7
  import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
9
8
  import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
10
9
  import { Button } from '@redocly/theme/components/Button/Button';
@@ -12,21 +11,9 @@ import { CursorIcon } from '@redocly/theme/icons/CursorIcon/CursorIcon';
12
11
  import { VSCodeIcon } from '@redocly/theme/icons/VSCodeIcon/VSCodeIcon';
13
12
  import { CopyIcon } from '@redocly/theme/icons/CopyIcon/CopyIcon';
14
13
  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';
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
- });
14
+ import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
15
+ import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
16
+ import { useConnectMCPButton, useOutsideClick, useThemeHooks } from '@redocly/theme/core/hooks';
30
17
 
31
18
  type MenuOption = {
32
19
  key: MCPOption;
@@ -74,57 +61,93 @@ export function ConnectMCPButton({
74
61
  placement = 'bottom',
75
62
  alignment = 'end',
76
63
  options = ['cursor', 'vscode', 'copy'],
77
- }: ConnectMCPButtonProps): JSX.Element {
64
+ }: ConnectMCPButtonProps): JSX.Element | null {
78
65
  const { useTranslate } = useThemeHooks();
79
66
  const { translate } = useTranslate();
67
+ const [isOpen, setIsOpen] = useState(false);
68
+ const wrapperRef = useRef<HTMLDivElement>(null);
80
69
 
81
- const { isCopied, triggerButtonText, visibleOptions, handleAction } = useConnectMCPButton({
82
- options,
83
- });
70
+ const { isCopied, visibleOptions, handleAction } = useConnectMCPButton({ options });
84
71
 
85
- const menuOptions = useMemo(
86
- () => MENU_OPTIONS.filter((option) => visibleOptions.includes(option.key)),
87
- [visibleOptions],
88
- );
72
+ useOutsideClick(wrapperRef, () => setIsOpen(false));
73
+
74
+ const menuOptions = MENU_OPTIONS.filter((option) => visibleOptions.includes(option.key));
75
+ const primaryOption = menuOptions[0];
76
+ const dropdownOptions = menuOptions.slice(1);
77
+
78
+ if (!primaryOption) return null;
79
+
80
+ const PrimaryIcon = primaryOption.icon;
81
+ // When copy is the primary action there is no dropdown item to show the copied state, so the
82
+ // primary button itself must reflect it.
83
+ const showPrimaryCopied = primaryOption.key === 'copy' && isCopied;
84
+
85
+ const handleMenuAction = (key: MCPOption) => {
86
+ handleAction(key);
87
+ setIsOpen(false);
88
+ };
89
89
 
90
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>
91
+ <ConnectMCPButtonWrapper ref={wrapperRef} data-component-name="Buttons/ConnectMCPButton">
92
+ <SplitButtonGroup>
93
+ <PrimaryButton
94
+ variant="secondary"
95
+ icon={
96
+ showPrimaryCopied ? (
97
+ <CheckmarkFilledIcon color="var(--color-success-base)" />
98
+ ) : (
99
+ <PrimaryIcon />
100
+ )
101
+ }
102
+ onClick={() => handleAction(primaryOption.key)}
103
+ >
104
+ {translate(primaryOption.titleTranslationKey, primaryOption.titleDefault)}
105
+ </PrimaryButton>
106
+ {dropdownOptions.length > 0 && (
107
+ <ChevronButton
108
+ variant="secondary"
109
+ aria-label={translate('page.actions.connectMcp.moreOptions', 'More options')}
110
+ aria-expanded={isOpen}
111
+ aria-haspopup="menu"
112
+ onClick={() => setIsOpen((prev) => !prev)}
113
+ >
114
+ {isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
115
+ </ChevronButton>
116
+ )}
117
+ </SplitButtonGroup>
118
+
119
+ {isOpen && dropdownOptions.length > 0 && (
120
+ <DropdownWrapper $placement={placement} $alignment={alignment}>
121
+ <DropdownMenu>
122
+ {dropdownOptions.map((option) => {
123
+ const Icon = option.icon;
124
+ const showCheckmark = option.key === 'copy' && isCopied;
125
+
126
+ return (
127
+ <DropdownMenuItem key={option.key} onAction={() => handleMenuAction(option.key)}>
128
+ <MenuItemContent>
129
+ <MenuItemIcon>
130
+ {showCheckmark ? (
131
+ <CheckmarkFilledIcon color="var(--color-success-base)" />
132
+ ) : (
133
+ <Icon />
134
+ )}
135
+ </MenuItemIcon>
136
+ <MenuItemText>
137
+ <MenuItemTitle>
138
+ {translate(option.titleTranslationKey, option.titleDefault)}
139
+ </MenuItemTitle>
140
+ <MenuItemDescription>
141
+ {translate(option.descriptionTranslationKey, option.descriptionDefault)}
142
+ </MenuItemDescription>
143
+ </MenuItemText>
144
+ </MenuItemContent>
145
+ </DropdownMenuItem>
146
+ );
147
+ })}
148
+ </DropdownMenu>
149
+ </DropdownWrapper>
150
+ )}
128
151
  </ConnectMCPButtonWrapper>
129
152
  );
130
153
  }
@@ -134,8 +157,32 @@ const ConnectMCPButtonWrapper = styled.div`
134
157
  position: relative;
135
158
  `;
136
159
 
137
- const StyledButton = styled(Button)`
160
+ const SplitButtonGroup = styled.div`
161
+ display: inline-flex;
162
+ align-items: stretch;
163
+ `;
164
+
165
+ const PrimaryButton = styled(Button)`
138
166
  --button-gap: var(--connect-mcp-button-gap);
167
+ border-top-right-radius: 0;
168
+ border-bottom-right-radius: 0;
169
+ `;
170
+
171
+ const ChevronButton = styled(Button)`
172
+ border-top-left-radius: 0;
173
+ border-bottom-left-radius: 0;
174
+ border-left: none;
175
+ padding-inline: var(--connect-mcp-button-chevron-padding-inline);
176
+ `;
177
+
178
+ const DropdownWrapper = styled.div<{ $placement?: string; $alignment?: string }>`
179
+ position: absolute;
180
+ top: ${({ $placement }) => ($placement === 'top' ? 'auto' : '100%')};
181
+ bottom: ${({ $placement }) => ($placement === 'top' ? '100%' : 'auto')};
182
+ left: ${({ $alignment }) => ($alignment === 'start' ? '0' : 'auto')};
183
+ right: ${({ $alignment }) => ($alignment === 'end' ? '0' : 'auto')};
184
+ padding-top: var(--dropdown-menu-padding-top);
185
+ z-index: var(--dropdown-menu-z-index);
139
186
  `;
140
187
 
141
188
  const MenuItemContent = styled.div`
@@ -58,6 +58,9 @@ export const connectMCPButton = css`
58
58
  /* Button gap */
59
59
  --connect-mcp-button-gap: var(--spacing-xs);
60
60
 
61
+ /* Chevron button */
62
+ --connect-mcp-button-chevron-padding-inline: var(--spacing-xs);
63
+
61
64
  /* Menu item layout */
62
65
  --connect-mcp-button-menu-item-gap: var(--spacing-sm);
63
66
  --connect-mcp-button-menu-item-padding-block: var(--spacing-xxs);
@@ -16,7 +16,7 @@ export type ReasonsProps = {
16
16
  };
17
17
 
18
18
  export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.Element | null {
19
- const { label, component, items = [] } = settings;
19
+ const { label, component = 'checkbox', items = [] } = settings;
20
20
  const [checkedState, setCheckedState] = React.useState(new Array(items.length).fill(false));
21
21
  const { useTranslate } = useThemeHooks();
22
22
  const { translate } = useTranslate();
@@ -25,8 +25,6 @@ export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.El
25
25
  return null;
26
26
  }
27
27
 
28
- const input_type = component || 'checkbox';
29
-
30
28
  const handleOptionChange = (position: number) => {
31
29
  const updatedCheckedState =
32
30
  component === 'checkbox'
@@ -53,7 +51,7 @@ export function Reasons({ settings, onChange, className }: ReasonsProps): JSX.El
53
51
  {items.map((reason, idx) => (
54
52
  <OptionWrapper key={reason}>
55
53
  <input
56
- type={input_type}
54
+ type={component}
57
55
  value={reason}
58
56
  checked={checkedState[idx]}
59
57
  name="reasons"
@@ -10,7 +10,7 @@ import { Link } from '@redocly/theme/components/Link/Link';
10
10
  import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
11
11
  import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronRightIcon';
12
12
  import { HttpTag } from '@redocly/theme/components/Tags/HttpTag';
13
- import { MenuItemType } from '@redocly/theme/core/constants';
13
+ import { DEFAULT_LINE_POSITION, MenuItemType } from '@redocly/theme/core/constants';
14
14
  import {
15
15
  getBaseDataAttributes,
16
16
  getMenuItemType,
@@ -178,7 +178,10 @@ export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Ele
178
178
  </MenuItemNestedWrapper>
179
179
  ) : null}
180
180
  {item.separatorLine ? (
181
- <MenuItemSeparatorLine $depth={depth} $linePosition={item.linePosition} />
181
+ <MenuItemSeparatorLine
182
+ $depth={depth}
183
+ $linePosition={item.linePosition ?? DEFAULT_LINE_POSITION}
184
+ />
182
185
  ) : null}
183
186
  </MenuItemWrapper>
184
187
  );
@@ -59,7 +59,7 @@ export function SearchAiDialog({
59
59
 
60
60
  const conversationEndRef = React.useRef<HTMLDivElement>(null);
61
61
 
62
- const suggestions = aiAssistant?.suggestions;
62
+ const suggestions = aiAssistant?.suggestions ?? [];
63
63
 
64
64
  const placeholder = isGeneratingResponse
65
65
  ? translate('search.ai.generatingResponse', 'Generating response...')
@@ -42,7 +42,7 @@ export function SearchAiResponse(props: SearchAiResponseProps): JSX.Element {
42
42
 
43
43
  let responseContainer = null;
44
44
 
45
- const suggestions = aiAssistant?.suggestions;
45
+ const suggestions = aiAssistant?.suggestions ?? [];
46
46
 
47
47
  const hasPendingOrReceivedResponse = response || isGeneratingResponse || error;
48
48
  if (hasPendingOrReceivedResponse) {
@@ -7,6 +7,7 @@ import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
7
7
  import { Button } from '@redocly/theme/components/Button/Button';
8
8
  import { SearchIcon } from '@redocly/theme/icons/SearchIcon/SearchIcon';
9
9
  import { breakpoints, getUserAgent } from '@redocly/theme/core/utils';
10
+ import { DEFAULT_KEYBOARD_SHORTCUTS } from '@redocly/theme/core/constants';
10
11
 
11
12
  export type SearchTriggerProps = {
12
13
  onClick: () => void;
@@ -18,8 +19,7 @@ export function SearchTrigger({ onClick, className }: SearchTriggerProps): JSX.E
18
19
  const { useTranslate } = useThemeHooks();
19
20
  const { translate } = useTranslate();
20
21
 
21
- const defaultKeyShortcut = '⌘+K,CTRL+K';
22
- const keyShortcuts = themeSettings?.search?.shortcuts ?? [defaultKeyShortcut];
22
+ const keyShortcuts = themeSettings?.search?.shortcuts ?? [DEFAULT_KEYBOARD_SHORTCUTS];
23
23
  let mainShortcutKey: string | null | undefined = null;
24
24
 
25
25
  if (keyShortcuts) {
@@ -52,7 +52,7 @@ export function SearchTrigger({ onClick, className }: SearchTriggerProps): JSX.E
52
52
  >
53
53
  <SearchIcon />
54
54
  {translate('search.navbar.label', 'Search')}
55
- {mainShortcutKey === defaultKeyShortcut ? (
55
+ {mainShortcutKey === DEFAULT_KEYBOARD_SHORTCUTS ? (
56
56
  <span>{isMac ? '⌘K' : 'Ctrl+K'}</span>
57
57
  ) : (
58
58
  <span>{mainShortcutKey}</span>
@@ -35,3 +35,5 @@ export const DEFAULT_COLOR_MODES = {
35
35
  LIGHT: 'light',
36
36
  DARK: 'dark',
37
37
  } as const;
38
+
39
+ export const DEFAULT_LINE_POSITION = 'top';
@@ -46,6 +46,8 @@ export const AI_SEARCH_MAX_MESSAGE_LENGTH = 45000;
46
46
 
47
47
  export const SEARCH_DEBOUNCE_TIME_MS = 300;
48
48
 
49
+ export const DEFAULT_KEYBOARD_SHORTCUTS = '⌘+K,CTRL+K';
50
+
49
51
  export const TOOL_CALL_DISPLAY_TEXT: Partial<
50
52
  Record<string, { inProgressText: string; completedText: string }>
51
53
  > = {
@@ -6,6 +6,7 @@ import { useThemeHooks } from '../use-theme-hooks';
6
6
  import { useThemeConfig } from '../use-theme-config';
7
7
  import { useSearchSession } from '../../contexts';
8
8
  import { getBaseDataAttributes } from '../../utils';
9
+ import { DEFAULT_KEYBOARD_SHORTCUTS } from '../../constants';
9
10
 
10
11
  export function useSearchDialog() {
11
12
  const [isOpen, setIsOpen] = useState(false);
@@ -14,7 +15,7 @@ export function useSearchDialog() {
14
15
  const { useTelemetry } = useThemeHooks();
15
16
  const telemetry = useTelemetry();
16
17
  const { refreshSearchSessionId } = useSearchSession();
17
- const keyShortcuts = themeSettings?.search?.shortcuts ?? ['⌘+K,CTRL+K'];
18
+ const keyShortcuts = themeSettings?.search?.shortcuts ?? [DEFAULT_KEYBOARD_SHORTCUTS];
18
19
  const hotKeys = keyShortcuts?.join(',');
19
20
 
20
21
  useEffect(() => {