@redocly/theme 0.62.0 → 0.63.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.
@@ -4,5 +4,6 @@ export type BadgeProps = PropsWithChildren<{
4
4
  color?: string;
5
5
  key?: string;
6
6
  className?: string;
7
+ icon?: string;
7
8
  }>;
8
- export declare function Badge(props: BadgeProps): JSX.Element;
9
+ export declare function Badge({ icon, children, ...props }: BadgeProps): JSX.Element;
@@ -32,6 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __rest = (this && this.__rest) || function (s, e) {
36
+ var t = {};
37
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
38
+ t[p] = s[p];
39
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
40
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
41
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
42
+ t[p[i]] = s[p[i]];
43
+ }
44
+ return t;
45
+ };
35
46
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
48
  };
@@ -39,8 +50,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
50
  exports.Badge = Badge;
40
51
  const react_1 = __importDefault(require("react"));
41
52
  const styled_components_1 = __importStar(require("styled-components"));
42
- function Badge(props) {
43
- return react_1.default.createElement(BadgeComponent, Object.assign({}, props, { "data-component-name": "Badge/Badge" }));
53
+ const GenericIcon_1 = require("../../icons/GenericIcon/GenericIcon");
54
+ function Badge(_a) {
55
+ var { icon, children } = _a, props = __rest(_a, ["icon", "children"]);
56
+ return (react_1.default.createElement(BadgeComponent, Object.assign({}, props, { "data-component-name": "Badge/Badge" }),
57
+ icon ? react_1.default.createElement(BadgeIcon, { icon: icon }) : null,
58
+ children));
44
59
  }
45
60
  const BadgeComponent = styled_components_1.default.span `
46
61
  display: inline-block;
@@ -61,4 +76,11 @@ const BadgeComponent = styled_components_1.default.span `
61
76
  border-radius: var(--badge-deprecated-border-radius);
62
77
  `}
63
78
  `;
79
+ const BadgeIcon = (0, styled_components_1.default)(GenericIcon_1.GenericIcon) `
80
+ --icon-width: var(--font-size-sm);
81
+ --icon-height: var(--font-size-sm);
82
+ margin-right: var(--spacing-xxs);
83
+ flex-shrink: 0;
84
+ vertical-align: middle;
85
+ `;
64
86
  //# sourceMappingURL=Badge.js.map
@@ -105,7 +105,7 @@ function AIAssistantButton() {
105
105
  const StyledAIAssistantButton = (0, styled_components_1.default)(Button_1.Button) `
106
106
  position: fixed;
107
107
  bottom: var(--ai-assistant-button-bottom);
108
- right: var(--ai-assistant-button-right);
108
+ right: calc(var(--ai-assistant-button-right) + var(--modal-scrollbar-width, 0px));
109
109
  ${({ $inputType }) => $inputType === 'icon'
110
110
  ? `
111
111
  border-radius: var(--ai-assistant-button-border-radius-icon);
@@ -45,10 +45,10 @@ const HttpTag_1 = require("../../components/Tags/HttpTag");
45
45
  const constants_1 = require("../../core/constants");
46
46
  const utils_1 = require("../../core/utils");
47
47
  const ArrowRightIcon_1 = require("../../icons/ArrowRightIcon/ArrowRightIcon");
48
- const Badge_1 = require("../../components/Badge/Badge");
49
48
  const GenericIcon_1 = require("../../icons/GenericIcon/GenericIcon");
49
+ const Tag_1 = require("../../components/Tag/Tag");
50
50
  function MenuItem(props) {
51
- var _a;
51
+ var _a, _b;
52
52
  const { item, depth, className, onClick } = props;
53
53
  const { useTranslate, useTelemetry } = (0, hooks_1.useThemeHooks)();
54
54
  const { translate } = useTranslate();
@@ -92,9 +92,10 @@ function MenuItem(props) {
92
92
  hasChevron ? react_1.default.createElement(ChevronWrapper, null, chevron) : null,
93
93
  react_1.default.createElement(MenuItemIcon, { icon: item.icon, srcSet: item.srcSet }),
94
94
  react_1.default.createElement(MenuItemLabelTextWrapper, null,
95
- react_1.default.createElement(MenuItemLabel, null,
96
- react_1.default.createElement("span", null, translate(item.labelTranslationKey, item.label)), (_a = item.badges) === null || _a === void 0 ? void 0 :
97
- _a.map(({ name, color }) => (react_1.default.createElement(MenuItemBadge, { color: color, key: name }, name))),
95
+ react_1.default.createElement(MenuItemLabel, null, (_a = item.badges) === null || _a === void 0 ? void 0 :
96
+ _a.filter(({ position }) => position === 'before').map(({ name, color, icon }) => (react_1.default.createElement(SidebarTag, { color: isDirectColorValue(color) ? undefined : color, "$bgColor": isDirectColorValue(color) ? color : undefined, key: name, icon: icon && react_1.default.createElement(BadgeIcon, { icon: icon }) }, name))),
97
+ react_1.default.createElement("span", null, translate(item.labelTranslationKey, item.label)), (_b = item.badges) === null || _b === void 0 ? void 0 :
98
+ _b.filter(({ position }) => position !== 'before').map(({ name, color, icon }) => (react_1.default.createElement(SidebarTag, { color: isDirectColorValue(color) ? undefined : color, "$bgColor": isDirectColorValue(color) ? color : undefined, key: name, icon: icon && react_1.default.createElement(BadgeIcon, { icon: icon }) }, name))),
98
99
  item.external ? react_1.default.createElement(LaunchIcon_1.LaunchIcon, { size: "var(--menu-item-external-icon-size)" }) : null),
99
100
  item.sublabel ? (react_1.default.createElement(MenuItemSubLabel, null, translate(item.subLabelTranslationKey, item.sublabel))) : null),
100
101
  isDrilldown ? react_1.default.createElement(ArrowRightIcon_1.ArrowRightIcon, { size: "12px" }) : null,
@@ -104,6 +105,12 @@ function MenuItem(props) {
104
105
  isNested ? (react_1.default.createElement(MenuItemNestedWrapper, { depth: depth, ref: nestedMenuRef, style: style }, isExpanded || !canUnmount ? props.children : null)) : null,
105
106
  item.separatorLine ? (react_1.default.createElement(MenuItemSeparatorLine, { depth: depth, linePosition: item.linePosition })) : null));
106
107
  }
108
+ /* for backward compatibility */
109
+ function isDirectColorValue(color) {
110
+ if (!color)
111
+ return false;
112
+ return color.startsWith('#') || color.startsWith('rgb') || color.startsWith('hsl');
113
+ }
107
114
  function generateClassName({ type, item, className, }) {
108
115
  const classNames = [className, `menu-item-type-${type}`];
109
116
  if (type === constants_1.MenuItemType.Separator) {
@@ -282,15 +289,31 @@ const MenuItemLabel = styled_components_1.default.span `
282
289
  margin-right: var(--spacing-xxs);
283
290
  }
284
291
  `;
285
- const MenuItemBadge = (0, styled_components_1.default)(Badge_1.Badge) `
292
+ const SidebarTag = (0, styled_components_1.default)(Tag_1.Tag) `
293
+ ${({ $bgColor }) => $bgColor && `background-color: ${$bgColor};`} /* for backward compatibility */
286
294
  margin-left: 0;
287
- background-color: ${({ color }) => color || 'var(--color-info-base)'};
288
295
  font-size: var(--font-size-sm);
289
296
  line-height: var(--line-height-sm);
290
297
  padding: 0 var(--spacing-xxs);
291
- max-width: 80px;
292
- text-overflow: ellipsis;
293
- white-space: nowrap;
294
- overflow: hidden;
298
+ max-width: 90px;
299
+
300
+ --tag-padding: 0 var(--spacing-xxs);
301
+ --tag-content-padding: 0;
302
+ --tag-font-size: var(--font-size-sm);
303
+ --tag-line-height: var(--line-height-sm);
304
+ vertical-align: middle;
305
+
306
+ ${Tag_1.ContentWrapper} {
307
+ text-overflow: ellipsis;
308
+ white-space: nowrap;
309
+ overflow: hidden;
310
+ display: block;
311
+ }
312
+ `;
313
+ const BadgeIcon = (0, styled_components_1.default)(GenericIcon_1.GenericIcon) `
314
+ --icon-width: var(--font-size-sm);
315
+ --icon-height: var(--font-size-sm);
316
+ margin-right: var(--spacing-xxs);
317
+ flex-shrink: 0;
295
318
  `;
296
319
  //# sourceMappingURL=MenuItem.js.map
@@ -131,12 +131,12 @@ exports.mobileMenu = (0, styled_components_1.css) `
131
131
  * @tokens Mobile Menu
132
132
  * */
133
133
  /* Fallback for older browsers. dvh accounts for dynamic UI elements like mobile address bars */
134
- --menu-mobile-height: calc(100vh - var(--navbar-height) - var(--banner-height));
135
- --menu-mobile-height: calc(100dvh - var(--navbar-height) - var(--banner-height));
134
+ --menu-mobile-height: calc(100vh - var(--navbar-height));
135
+ --menu-mobile-height: calc(100dvh - var(--navbar-height));
136
136
  --menu-mobile-width: 100%;
137
137
  --menu-mobile-z-index: var(--z-index-raised);
138
138
  --menu-mobile-left: 0;
139
- --menu-mobile-top: calc(var(--navbar-height) + var(--banner-height));
139
+ --menu-mobile-top: var(--navbar-height);
140
140
  --menu-mobile-transition: 0.5s;
141
141
  --menu-mobile-bg: var(--bg-color); // @presenter Color
142
142
  --menu-mobile-margin: var(--menu-mobile-items-margin-top) var(--menu-mobile-margin-horizontal) 0 var(--menu-mobile-margin-horizontal);
@@ -49,6 +49,7 @@ exports.PageActions = PageActions;
49
49
  const react_1 = __importStar(require("react"));
50
50
  const styled_components_1 = __importDefault(require("styled-components"));
51
51
  const PageActionsMenuItem_1 = require("../../components/PageActions/PageActionsMenuItem");
52
+ const DropdownMenuItem_1 = require("../../components/Dropdown/DropdownMenuItem");
52
53
  const Link_1 = require("../../components/Link/Link");
53
54
  const ButtonGroup_1 = require("../../components/Button/ButtonGroup");
54
55
  const Button_1 = require("../../components/Button/Button");
@@ -77,16 +78,18 @@ function PageActions(props) {
77
78
  setActionState('idle');
78
79
  }, ACTION_DONE_DISPLAY_DURATION);
79
80
  });
80
- const menuItems = actions.map((action) => ({
81
- content: 'link' in action ? (react_1.default.createElement(LinkMenuItem, { to: action.link, external: true },
82
- react_1.default.createElement(PageActionsMenuItem_1.PageActionsMenuItem, { pageAction: action }))) : (react_1.default.createElement(PageActionsMenuItem_1.PageActionsMenuItem, { pageAction: action })),
83
- onAction: 'onClick' in action ? () => handleActionClick(action) : undefined,
84
- }));
85
- return (react_1.default.createElement(PageActionsWrapper, null,
81
+ const menuItems = actions.map((action, index) => {
82
+ const key = `${action.title}-${index}`;
83
+ const hasLink = 'link' in action;
84
+ const content = hasLink ? (react_1.default.createElement(LinkMenuItem, { key: `${key}-link`, to: action.link, tabIndex: -1, external: true },
85
+ react_1.default.createElement(PageActionsMenuItem_1.PageActionsMenuItem, { pageAction: action }))) : (react_1.default.createElement(PageActionsMenuItem_1.PageActionsMenuItem, { pageAction: action }));
86
+ return (react_1.default.createElement(StyledDropdownMenuItem, { key: key, onAction: () => handleActionClick(action) }, content));
87
+ });
88
+ return (react_1.default.createElement(PageActionsWrapper, { "data-component-name": "PageActions/PageActions" },
86
89
  react_1.default.createElement(ButtonGroup_1.ButtonGroup, { variant: "outlined", size: "medium" },
87
90
  react_1.default.createElement(Button_1.Button, { icon: renderIcon(buttonAction, actionState), to: 'link' in buttonAction ? buttonAction.link : undefined, external: true, onClick: () => handleActionClick(buttonAction) }, buttonAction.buttonText),
88
- actions.length > 1 ? (react_1.default.createElement(Dropdown_1.Dropdown, { withArrow: true, trigger: react_1.default.createElement(Button_1.Button, null), placement: "bottom", alignment: "end" },
89
- react_1.default.createElement(StyledDropdownMenu, { items: menuItems }))) : null)));
91
+ actions.length > 1 ? (react_1.default.createElement(StyledDropdown, { withArrow: true, trigger: react_1.default.createElement(Button_1.Button, null), placement: "bottom", alignment: "end" },
92
+ react_1.default.createElement(StyledDropdownMenu, null, menuItems))) : null)));
90
93
  }
91
94
  function renderIcon(buttonAction, actionState) {
92
95
  switch (actionState) {
@@ -111,7 +114,21 @@ const LinkMenuItem = (0, styled_components_1.default)(Link_1.Link) `
111
114
  text-decoration: none;
112
115
  --link-decoration-hover: none;
113
116
  `;
117
+ const StyledDropdown = (0, styled_components_1.default)(Dropdown_1.Dropdown) `
118
+ z-index: calc(var(--z-index-raised) - 1);
119
+ `;
114
120
  const StyledDropdownMenu = (0, styled_components_1.default)(DropdownMenu_1.DropdownMenu) `
115
121
  --dropdown-menu-max-height: var(--page-actions-dropdown-max-height);
116
122
  `;
123
+ const StyledDropdownMenuItem = (0, styled_components_1.default)(DropdownMenuItem_1.DropdownMenuItem) `
124
+ &:has(a) {
125
+ padding: 0;
126
+ & > a {
127
+ display: inline-block;
128
+ width: 100%;
129
+ padding: var(--dropdown-menu-item-padding-vertical)
130
+ var(--dropdown-menu-item-padding-horizontal);
131
+ }
132
+ }
133
+ `;
117
134
  //# sourceMappingURL=PageActions.js.map
@@ -74,6 +74,7 @@ function SearchDialog({ onClose, className, initialMode = 'search', }) {
74
74
  const { isFilterOpen, onFilterToggle, onFilterChange, onFilterReset, onFacetReset, onQuickFilterReset, } = (0, hooks_1.useSearchFilter)(filter, setFilter);
75
75
  const { addSearchHistoryItem } = (0, hooks_1.useRecentSearches)();
76
76
  const aiSearch = useAiSearch({ filter });
77
+ (0, hooks_1.useModalScrollLock)(true);
77
78
  const searchInputRef = (0, react_1.useRef)(null);
78
79
  const modalRef = (0, react_1.useRef)(null);
79
80
  const [isMobile, setIsMobile] = (0, react_1.useState)(false);
@@ -6,6 +6,7 @@ const use_theme_hooks_1 = require("./use-theme-hooks");
6
6
  const dom_1 = require("../utils/dom");
7
7
  const constants_1 = require("../constants");
8
8
  const mcp_1 = require("../utils/mcp");
9
+ const urls_1 = require("../utils/urls");
9
10
  function useMCPConfig() {
10
11
  var _a;
11
12
  const { useMcpData } = (0, use_theme_hooks_1.useThemeHooks)();
@@ -14,7 +15,7 @@ function useMCPConfig() {
14
15
  ? window.location.origin
15
16
  : ((_a = globalThis['SSR_HOSTNAME']) !== null && _a !== void 0 ? _a : '');
16
17
  const serverName = name || constants_1.DEFAULT_MCP_SERVER_NAME;
17
- const serverUrl = `${origin}/mcp`;
18
+ const serverUrl = `${origin}${(0, urls_1.withPathPrefix)('/mcp')}`;
18
19
  const isMcpDisabled = !enabled || false;
19
20
  const cursorUrl = (0, react_1.useMemo)(() => (0, mcp_1.generateMCPDeepLink)('cursor', { serverName, url: serverUrl }), [serverName, serverUrl]);
20
21
  const vscodeUrl = (0, react_1.useMemo)(() => (0, mcp_1.generateMCPDeepLink)('vscode', { serverName, url: serverUrl }), [serverName, serverUrl]);
@@ -4,19 +4,33 @@ exports.useModalScrollLock = useModalScrollLock;
4
4
  const react_1 = require("react");
5
5
  function useModalScrollLock(isOpen) {
6
6
  (0, react_1.useEffect)(() => {
7
- const originalOverflow = document.body.style.overflow;
8
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
9
- if (isOpen) {
10
- document.body.style.overflow = 'hidden';
11
- document.body.style.marginRight = `${scrollbarWidth}px`;
7
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
8
+ return;
12
9
  }
13
- else {
14
- document.body.style.overflow = originalOverflow;
15
- document.body.style.marginRight = '';
10
+ const { body, documentElement } = document;
11
+ const originalOverflow = body.style.overflow;
12
+ const originalPaddingRight = body.style.paddingRight;
13
+ const originalScrollbarWidth = documentElement.style.getPropertyValue('--modal-scrollbar-width');
14
+ const restoreScrollState = () => {
15
+ body.style.overflow = originalOverflow;
16
+ body.style.paddingRight = originalPaddingRight;
17
+ if (originalScrollbarWidth) {
18
+ documentElement.style.setProperty('--modal-scrollbar-width', originalScrollbarWidth);
19
+ }
20
+ else {
21
+ documentElement.style.removeProperty('--modal-scrollbar-width');
22
+ }
23
+ };
24
+ if (isOpen) {
25
+ const scrollbarWidth = window.innerWidth - documentElement.clientWidth;
26
+ body.style.overflow = 'hidden';
27
+ if (scrollbarWidth > 0) {
28
+ body.style.paddingRight = `${scrollbarWidth}px`;
29
+ documentElement.style.setProperty('--modal-scrollbar-width', `${scrollbarWidth}px`);
30
+ }
16
31
  }
17
32
  return () => {
18
- document.body.style.overflow = originalOverflow;
19
- document.body.style.marginRight = '';
33
+ restoreScrollState();
20
34
  };
21
35
  }, [isOpen]);
22
36
  }
@@ -119,22 +119,25 @@ function usePageActions(pageSlug, mcpUrl, actions) {
119
119
  telemetry.sendPageActionsButtonClickedMessage([
120
120
  Object.assign(Object.assign({}, createPageActionResource(pageSlug, pageUrl)), { action_type: 'view' }),
121
121
  ]);
122
+ window.location.href = mdPageUrl;
122
123
  },
123
124
  }),
124
125
  chatgpt: () => {
125
126
  if (!isPublic) {
126
127
  return null;
127
128
  }
129
+ const link = getExternalAiPromptLink('https://chat.openai.com', mdPageUrl);
128
130
  return {
129
131
  buttonText: translate('page.actions.chatGptButtonText', 'Open in ChatGPT'),
130
132
  title: translate('page.actions.chatGptTitle', 'Open in ChatGPT'),
131
133
  description: translate('page.actions.chatGptDescription', 'Get insights from ChatGPT'),
132
134
  iconComponent: ChatGptIcon_1.ChatGptIcon,
133
- link: getExternalAiPromptLink('https://chat.openai.com', mdPageUrl),
135
+ link,
134
136
  onClick: () => {
135
137
  telemetry.sendPageActionsButtonClickedMessage([
136
138
  Object.assign(Object.assign({}, createPageActionResource(pageSlug, pageUrl)), { action_type: 'chatgpt' }),
137
139
  ]);
140
+ window.location.href = link;
138
141
  },
139
142
  };
140
143
  },
@@ -142,16 +145,18 @@ function usePageActions(pageSlug, mcpUrl, actions) {
142
145
  if (!isPublic) {
143
146
  return null;
144
147
  }
148
+ const link = getExternalAiPromptLink('https://claude.ai/new', mdPageUrl);
145
149
  return {
146
150
  buttonText: translate('page.actions.claudeButtonText', 'Open in Claude'),
147
151
  title: translate('page.actions.claudeTitle', 'Open in Claude'),
148
152
  description: translate('page.actions.claudeDescription', 'Get insights from Claude'),
149
153
  iconComponent: ClaudeIcon_1.ClaudeIcon,
150
- link: getExternalAiPromptLink('https://claude.ai/new', mdPageUrl),
154
+ link,
151
155
  onClick: () => {
152
156
  telemetry.sendPageActionsButtonClickedMessage([
153
157
  Object.assign(Object.assign({}, createPageActionResource(pageSlug, pageUrl)), { action_type: 'claude' }),
154
158
  ]);
159
+ window.location.href = link;
155
160
  },
156
161
  };
157
162
  },
@@ -1090,10 +1090,6 @@ const error = (0, styled_components_1.css) `
1090
1090
  --compilation-error-file-header-margin: 0 0 var(--spacing-xs) 0;
1091
1091
  `;
1092
1092
  const modal = (0, styled_components_1.css) `
1093
- body:has(.scroll-lock) {
1094
- overflow: hidden;
1095
- }
1096
-
1097
1093
  --modal-box-shadow: var(--bg-raised-shadow);
1098
1094
  --modal-bg-color: var(--bg-color);
1099
1095
  `;
@@ -46,7 +46,7 @@ const FilterWrapper = styled_components_1.default.div `
46
46
  padding-bottom: var(--spacing-xs);
47
47
  top: var(--navbar-height);
48
48
  background-color: var(--bg-color);
49
- z-index: 1;
49
+ z-index: var(--z-index-raised);
50
50
  max-width: var(--md-content-max-width);
51
51
  `;
52
52
  const ButtonsWrapper = styled_components_1.default.div `
@@ -97,6 +97,12 @@ const HeadingContentWrapper = styled_components_1.default.div `
97
97
  align-items: flex-start;
98
98
  gap: var(--spacing-sm);
99
99
  }
100
+
101
+ &:has([data-component-name='PageActions/PageActions']:hover) {
102
+ && .anchor svg {
103
+ visibility: hidden;
104
+ }
105
+ }
100
106
  `;
101
107
  const StyledHeadingText = styled_components_1.default.span `
102
108
  margin: auto 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.62.0",
3
+ "version": "0.63.0-next.0",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -63,7 +63,7 @@
63
63
  "vitest": "4.0.10",
64
64
  "vitest-when": "0.6.2",
65
65
  "webpack": "5.94.0",
66
- "@redocly/realm-asyncapi-sdk": "0.8.0"
66
+ "@redocly/realm-asyncapi-sdk": "0.9.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.6.2",
82
82
  "react-calendar": "5.1.0",
83
83
  "react-date-picker": "11.0.0",
84
- "@redocly/config": "0.42.0"
84
+ "@redocly/config": "0.43.0"
85
85
  },
86
86
  "scripts": {
87
87
  "watch": "tsc -p tsconfig.build.json && (concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\")",
@@ -3,15 +3,23 @@ import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { PropsWithChildren, JSX } from 'react';
5
5
 
6
+ import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon';
7
+
6
8
  export type BadgeProps = PropsWithChildren<{
7
9
  deprecated?: boolean;
8
10
  color?: string;
9
11
  key?: string;
10
12
  className?: string;
13
+ icon?: string;
11
14
  }>;
12
15
 
13
- export function Badge(props: BadgeProps): JSX.Element {
14
- return <BadgeComponent {...props} data-component-name="Badge/Badge" />;
16
+ export function Badge({ icon, children, ...props }: BadgeProps): JSX.Element {
17
+ return (
18
+ <BadgeComponent {...props} data-component-name="Badge/Badge">
19
+ {icon ? <BadgeIcon icon={icon} /> : null}
20
+ {children}
21
+ </BadgeComponent>
22
+ );
15
23
  }
16
24
 
17
25
  const BadgeComponent = styled.span<BadgeProps>`
@@ -34,3 +42,11 @@ const BadgeComponent = styled.span<BadgeProps>`
34
42
  border-radius: var(--badge-deprecated-border-radius);
35
43
  `}
36
44
  `;
45
+
46
+ const BadgeIcon = styled(GenericIcon)`
47
+ --icon-width: var(--font-size-sm);
48
+ --icon-height: var(--font-size-sm);
49
+ margin-right: var(--spacing-xxs);
50
+ flex-shrink: 0;
51
+ vertical-align: middle;
52
+ `;
@@ -111,7 +111,7 @@ export function AIAssistantButton() {
111
111
  const StyledAIAssistantButton = styled(Button)<{ $inputType?: AIAssistantButtonType }>`
112
112
  position: fixed;
113
113
  bottom: var(--ai-assistant-button-bottom);
114
- right: var(--ai-assistant-button-right);
114
+ right: calc(var(--ai-assistant-button-right) + var(--modal-scrollbar-width, 0px));
115
115
  ${({ $inputType }) =>
116
116
  $inputType === 'icon'
117
117
  ? `
@@ -13,8 +13,8 @@ import { HttpTag } from '@redocly/theme/components/Tags/HttpTag';
13
13
  import { MenuItemType } from '@redocly/theme/core/constants';
14
14
  import { getMenuItemType, getOperationColor } from '@redocly/theme/core/utils';
15
15
  import { ArrowRightIcon } from '@redocly/theme/icons/ArrowRightIcon/ArrowRightIcon';
16
- import { Badge } from '@redocly/theme/components/Badge/Badge';
17
16
  import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon';
17
+ import { Tag, ContentWrapper } from '@redocly/theme/components/Tag/Tag';
18
18
 
19
19
  export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Element {
20
20
  const { item, depth, className, onClick } = props;
@@ -95,12 +95,31 @@ export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Ele
95
95
  <MenuItemIcon icon={item.icon} srcSet={item.srcSet} />
96
96
  <MenuItemLabelTextWrapper>
97
97
  <MenuItemLabel>
98
+ {item.badges
99
+ ?.filter(({ position }) => position === 'before')
100
+ .map(({ name, color, icon }) => (
101
+ <SidebarTag
102
+ color={isDirectColorValue(color) ? undefined : color}
103
+ $bgColor={isDirectColorValue(color) ? color : undefined}
104
+ key={name}
105
+ icon={icon && <BadgeIcon icon={icon} />}
106
+ >
107
+ {name}
108
+ </SidebarTag>
109
+ ))}
98
110
  <span>{translate(item.labelTranslationKey, item.label)}</span>
99
- {item.badges?.map(({ name, color }) => (
100
- <MenuItemBadge color={color} key={name}>
101
- {name}
102
- </MenuItemBadge>
103
- ))}
111
+ {item.badges
112
+ ?.filter(({ position }) => position !== 'before')
113
+ .map(({ name, color, icon }) => (
114
+ <SidebarTag
115
+ color={isDirectColorValue(color) ? undefined : color}
116
+ $bgColor={isDirectColorValue(color) ? color : undefined}
117
+ key={name}
118
+ icon={icon && <BadgeIcon icon={icon} />}
119
+ >
120
+ {name}
121
+ </SidebarTag>
122
+ ))}
104
123
  {item.external ? <LaunchIcon size="var(--menu-item-external-icon-size)" /> : null}
105
124
  </MenuItemLabel>
106
125
  {item.sublabel ? (
@@ -149,6 +168,12 @@ export function MenuItem(props: React.PropsWithChildren<MenuItemProps>): JSX.Ele
149
168
  );
150
169
  }
151
170
 
171
+ /* for backward compatibility */
172
+ function isDirectColorValue(color?: string) {
173
+ if (!color) return false;
174
+ return color.startsWith('#') || color.startsWith('rgb') || color.startsWith('hsl');
175
+ }
176
+
152
177
  function generateClassName({
153
178
  type,
154
179
  item,
@@ -362,14 +387,31 @@ const MenuItemLabel = styled.span`
362
387
  }
363
388
  `;
364
389
 
365
- const MenuItemBadge = styled(Badge)<{ color?: string }>`
390
+ const SidebarTag = styled(Tag)<{ $bgColor?: string; icon?: React.ReactNode }>`
391
+ ${({ $bgColor }) => $bgColor && `background-color: ${$bgColor};`} /* for backward compatibility */
366
392
  margin-left: 0;
367
- background-color: ${({ color }) => color || 'var(--color-info-base)'};
368
393
  font-size: var(--font-size-sm);
369
394
  line-height: var(--line-height-sm);
370
395
  padding: 0 var(--spacing-xxs);
371
- max-width: 80px;
372
- text-overflow: ellipsis;
373
- white-space: nowrap;
374
- overflow: hidden;
396
+ max-width: 90px;
397
+
398
+ --tag-padding: 0 var(--spacing-xxs);
399
+ --tag-content-padding: 0;
400
+ --tag-font-size: var(--font-size-sm);
401
+ --tag-line-height: var(--line-height-sm);
402
+ vertical-align: middle;
403
+
404
+ ${ContentWrapper} {
405
+ text-overflow: ellipsis;
406
+ white-space: nowrap;
407
+ overflow: hidden;
408
+ display: block;
409
+ }
410
+ `;
411
+
412
+ const BadgeIcon = styled(GenericIcon)`
413
+ --icon-width: var(--font-size-sm);
414
+ --icon-height: var(--font-size-sm);
415
+ margin-right: var(--spacing-xxs);
416
+ flex-shrink: 0;
375
417
  `;
@@ -130,12 +130,12 @@ export const mobileMenu = css`
130
130
  * @tokens Mobile Menu
131
131
  * */
132
132
  /* Fallback for older browsers. dvh accounts for dynamic UI elements like mobile address bars */
133
- --menu-mobile-height: calc(100vh - var(--navbar-height) - var(--banner-height));
134
- --menu-mobile-height: calc(100dvh - var(--navbar-height) - var(--banner-height));
133
+ --menu-mobile-height: calc(100vh - var(--navbar-height));
134
+ --menu-mobile-height: calc(100dvh - var(--navbar-height));
135
135
  --menu-mobile-width: 100%;
136
136
  --menu-mobile-z-index: var(--z-index-raised);
137
137
  --menu-mobile-left: 0;
138
- --menu-mobile-top: calc(var(--navbar-height) + var(--banner-height));
138
+ --menu-mobile-top: var(--navbar-height);
139
139
  --menu-mobile-transition: 0.5s;
140
140
  --menu-mobile-bg: var(--bg-color); // @presenter Color
141
141
  --menu-mobile-margin: var(--menu-mobile-items-margin-top) var(--menu-mobile-margin-horizontal) 0 var(--menu-mobile-margin-horizontal);
@@ -3,9 +3,9 @@ import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
5
  import type { PageAction } from '@redocly/theme/core/types';
6
- import type { DropdownMenuItemProps } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
7
6
 
8
7
  import { PageActionsMenuItem } from '@redocly/theme/components/PageActions/PageActionsMenuItem';
8
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
9
9
  import { Link } from '@redocly/theme/components/Link/Link';
10
10
  import { ButtonGroup } from '@redocly/theme/components/Button/ButtonGroup';
11
11
  import { Button } from '@redocly/theme/components/Button/Button';
@@ -52,20 +52,27 @@ export function PageActions(props: PageActionProps): JSX.Element | null {
52
52
  }, ACTION_DONE_DISPLAY_DURATION);
53
53
  };
54
54
 
55
- const menuItems: DropdownMenuItemProps[] = actions.map((action) => ({
56
- content:
57
- 'link' in action ? (
58
- <LinkMenuItem to={action.link} external>
59
- <PageActionsMenuItem pageAction={action} />
60
- </LinkMenuItem>
61
- ) : (
55
+ const menuItems = actions.map((action, index) => {
56
+ const key = `${action.title}-${index}`;
57
+ const hasLink = 'link' in action;
58
+
59
+ const content = hasLink ? (
60
+ <LinkMenuItem key={`${key}-link`} to={action.link} tabIndex={-1} external>
62
61
  <PageActionsMenuItem pageAction={action} />
63
- ),
64
- onAction: 'onClick' in action ? () => handleActionClick(action) : undefined,
65
- }));
62
+ </LinkMenuItem>
63
+ ) : (
64
+ <PageActionsMenuItem pageAction={action} />
65
+ );
66
+
67
+ return (
68
+ <StyledDropdownMenuItem key={key} onAction={() => handleActionClick(action)}>
69
+ {content}
70
+ </StyledDropdownMenuItem>
71
+ );
72
+ });
66
73
 
67
74
  return (
68
- <PageActionsWrapper>
75
+ <PageActionsWrapper data-component-name="PageActions/PageActions">
69
76
  <ButtonGroup variant="outlined" size="medium">
70
77
  <Button
71
78
  icon={renderIcon(buttonAction, actionState)}
@@ -76,9 +83,9 @@ export function PageActions(props: PageActionProps): JSX.Element | null {
76
83
  {buttonAction.buttonText}
77
84
  </Button>
78
85
  {actions.length > 1 ? (
79
- <Dropdown withArrow trigger={<Button />} placement="bottom" alignment="end">
80
- <StyledDropdownMenu items={menuItems} />
81
- </Dropdown>
86
+ <StyledDropdown withArrow trigger={<Button />} placement="bottom" alignment="end">
87
+ <StyledDropdownMenu>{menuItems}</StyledDropdownMenu>
88
+ </StyledDropdown>
82
89
  ) : null}
83
90
  </ButtonGroup>
84
91
  </PageActionsWrapper>
@@ -111,6 +118,22 @@ const LinkMenuItem = styled(Link)`
111
118
  --link-decoration-hover: none;
112
119
  `;
113
120
 
121
+ const StyledDropdown = styled(Dropdown)`
122
+ z-index: calc(var(--z-index-raised) - 1);
123
+ `;
124
+
114
125
  const StyledDropdownMenu = styled(DropdownMenu)`
115
126
  --dropdown-menu-max-height: var(--page-actions-dropdown-max-height);
116
127
  `;
128
+
129
+ const StyledDropdownMenuItem = styled(DropdownMenuItem)`
130
+ &:has(a) {
131
+ padding: 0;
132
+ & > a {
133
+ display: inline-block;
134
+ width: 100%;
135
+ padding: var(--dropdown-menu-item-padding-vertical)
136
+ var(--dropdown-menu-item-padding-horizontal);
137
+ }
138
+ }
139
+ `;
@@ -16,6 +16,7 @@ import {
16
16
  useDialogHotKeys,
17
17
  useSearchFilter,
18
18
  useRecentSearches,
19
+ useModalScrollLock,
19
20
  } from '@redocly/theme/core/hooks';
20
21
  import { Tag } from '@redocly/theme/components/Tag/Tag';
21
22
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
@@ -75,7 +76,7 @@ export function SearchDialog({
75
76
  } = useSearchFilter(filter, setFilter);
76
77
  const { addSearchHistoryItem } = useRecentSearches();
77
78
  const aiSearch = useAiSearch({ filter });
78
-
79
+ useModalScrollLock(true);
79
80
  const searchInputRef = useRef<HTMLInputElement>(null);
80
81
  const modalRef = useRef<HTMLDivElement>(null);
81
82
 
@@ -4,6 +4,7 @@ import { useThemeHooks } from './use-theme-hooks';
4
4
  import { IS_BROWSER } from '../utils/dom';
5
5
  import { DEFAULT_MCP_SERVER_NAME } from '../constants';
6
6
  import { generateMCPDeepLink } from '../utils/mcp';
7
+ import { withPathPrefix } from '../utils/urls';
7
8
 
8
9
  export type McpConfig = {
9
10
  serverName: string;
@@ -25,7 +26,7 @@ export function useMCPConfig(): McpConfig {
25
26
  ? window.location.origin
26
27
  : ((globalThis as { SSR_HOSTNAME?: string })['SSR_HOSTNAME'] ?? '');
27
28
  const serverName = name || DEFAULT_MCP_SERVER_NAME;
28
- const serverUrl = `${origin}/mcp`;
29
+ const serverUrl = `${origin}${withPathPrefix('/mcp')}`;
29
30
  const isMcpDisabled = !enabled || false;
30
31
 
31
32
  const cursorUrl = useMemo(
@@ -1,21 +1,40 @@
1
1
  import { useEffect } from 'react';
2
2
 
3
- export function useModalScrollLock(isOpen: boolean) {
3
+ export function useModalScrollLock(isOpen: boolean): void {
4
4
  useEffect(() => {
5
- const originalOverflow = document.body.style.overflow;
6
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
5
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
6
+ return;
7
+ }
8
+
9
+ const { body, documentElement } = document;
10
+ const originalOverflow = body.style.overflow;
11
+ const originalPaddingRight = body.style.paddingRight;
12
+ const originalScrollbarWidth =
13
+ documentElement.style.getPropertyValue('--modal-scrollbar-width');
14
+
15
+ const restoreScrollState = () => {
16
+ body.style.overflow = originalOverflow;
17
+ body.style.paddingRight = originalPaddingRight;
18
+ if (originalScrollbarWidth) {
19
+ documentElement.style.setProperty('--modal-scrollbar-width', originalScrollbarWidth);
20
+ } else {
21
+ documentElement.style.removeProperty('--modal-scrollbar-width');
22
+ }
23
+ };
7
24
 
8
25
  if (isOpen) {
9
- document.body.style.overflow = 'hidden';
10
- document.body.style.marginRight = `${scrollbarWidth}px`;
11
- } else {
12
- document.body.style.overflow = originalOverflow;
13
- document.body.style.marginRight = '';
26
+ const scrollbarWidth = window.innerWidth - documentElement.clientWidth;
27
+
28
+ body.style.overflow = 'hidden';
29
+
30
+ if (scrollbarWidth > 0) {
31
+ body.style.paddingRight = `${scrollbarWidth}px`;
32
+ documentElement.style.setProperty('--modal-scrollbar-width', `${scrollbarWidth}px`);
33
+ }
14
34
  }
15
35
 
16
36
  return () => {
17
- document.body.style.overflow = originalOverflow;
18
- document.body.style.marginRight = '';
37
+ restoreScrollState();
19
38
  };
20
39
  }, [isOpen]);
21
40
  }
@@ -158,6 +158,7 @@ export function usePageActions(
158
158
  action_type: 'view',
159
159
  },
160
160
  ]);
161
+ window.location.href = mdPageUrl;
161
162
  },
162
163
  }),
163
164
 
@@ -165,12 +166,13 @@ export function usePageActions(
165
166
  if (!isPublic) {
166
167
  return null;
167
168
  }
169
+ const link = getExternalAiPromptLink('https://chat.openai.com', mdPageUrl);
168
170
  return {
169
171
  buttonText: translate('page.actions.chatGptButtonText', 'Open in ChatGPT'),
170
172
  title: translate('page.actions.chatGptTitle', 'Open in ChatGPT'),
171
173
  description: translate('page.actions.chatGptDescription', 'Get insights from ChatGPT'),
172
174
  iconComponent: ChatGptIcon,
173
- link: getExternalAiPromptLink('https://chat.openai.com', mdPageUrl),
175
+ link,
174
176
  onClick: () => {
175
177
  telemetry.sendPageActionsButtonClickedMessage([
176
178
  {
@@ -178,6 +180,7 @@ export function usePageActions(
178
180
  action_type: 'chatgpt',
179
181
  },
180
182
  ]);
183
+ window.location.href = link;
181
184
  },
182
185
  };
183
186
  },
@@ -186,12 +189,13 @@ export function usePageActions(
186
189
  if (!isPublic) {
187
190
  return null;
188
191
  }
192
+ const link = getExternalAiPromptLink('https://claude.ai/new', mdPageUrl);
189
193
  return {
190
194
  buttonText: translate('page.actions.claudeButtonText', 'Open in Claude'),
191
195
  title: translate('page.actions.claudeTitle', 'Open in Claude'),
192
196
  description: translate('page.actions.claudeDescription', 'Get insights from Claude'),
193
197
  iconComponent: ClaudeIcon,
194
- link: getExternalAiPromptLink('https://claude.ai/new', mdPageUrl),
198
+ link,
195
199
  onClick: () => {
196
200
  telemetry.sendPageActionsButtonClickedMessage([
197
201
  {
@@ -199,6 +203,7 @@ export function usePageActions(
199
203
  action_type: 'claude',
200
204
  },
201
205
  ]);
206
+ window.location.href = link;
202
207
  },
203
208
  };
204
209
  },
@@ -1104,10 +1104,6 @@ const error = css`
1104
1104
  `;
1105
1105
 
1106
1106
  const modal = css`
1107
- body:has(.scroll-lock) {
1108
- overflow: hidden;
1109
- }
1110
-
1111
1107
  --modal-box-shadow: var(--bg-raised-shadow);
1112
1108
  --modal-bg-color: var(--bg-color);
1113
1109
  `;
@@ -78,7 +78,7 @@ const FilterWrapper = styled.div`
78
78
  padding-bottom: var(--spacing-xs);
79
79
  top: var(--navbar-height);
80
80
  background-color: var(--bg-color);
81
- z-index: 1;
81
+ z-index: var(--z-index-raised);
82
82
  max-width: var(--md-content-max-width);
83
83
  `;
84
84
 
@@ -102,6 +102,12 @@ const HeadingContentWrapper = styled.div`
102
102
  align-items: flex-start;
103
103
  gap: var(--spacing-sm);
104
104
  }
105
+
106
+ &:has([data-component-name='PageActions/PageActions']:hover) {
107
+ && .anchor svg {
108
+ visibility: hidden;
109
+ }
110
+ }
105
111
  `;
106
112
 
107
113
  const StyledHeadingText = styled.span`