@redocly/theme 0.71.0-next.4 → 0.71.0-next.6

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 (80) hide show
  1. package/lib/components/Buttons/MobileMenuButton.d.ts +8 -0
  2. package/lib/components/Buttons/MobileMenuButton.js +58 -0
  3. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +1 -1
  4. package/lib/components/Feedback/Mood.js +1 -1
  5. package/lib/components/Feedback/Rating.js +1 -1
  6. package/lib/components/Feedback/Scale.js +1 -1
  7. package/lib/components/Feedback/Sentiment.js +1 -1
  8. package/lib/components/Filter/Filter.js +1 -1
  9. package/lib/components/MCP/McpConnectionTabs.d.ts +1 -1
  10. package/lib/components/MCP/McpConnectionTabs.js +24 -1
  11. package/lib/components/MissingTranslationNotice/MissingTranslationNotice.d.ts +11 -0
  12. package/lib/components/MissingTranslationNotice/MissingTranslationNotice.js +83 -0
  13. package/lib/components/MissingTranslationNotice/variables.d.ts +1 -0
  14. package/lib/components/MissingTranslationNotice/variables.dark.d.ts +1 -0
  15. package/lib/components/MissingTranslationNotice/variables.dark.js +10 -0
  16. package/lib/components/MissingTranslationNotice/variables.js +29 -0
  17. package/lib/components/Navbar/Navbar.js +10 -18
  18. package/lib/components/PageActions/PageActions.js +1 -0
  19. package/lib/components/Select/Select.js +1 -2
  20. package/lib/core/hooks/use-active-section-id.js +8 -0
  21. package/lib/core/hooks/use-focus-trap.js +6 -5
  22. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  23. package/lib/core/hooks/use-page-actions.js +47 -35
  24. package/lib/core/styles/dark.js +4 -2
  25. package/lib/core/styles/global.js +70 -68
  26. package/lib/core/types/hooks.d.ts +2 -5
  27. package/lib/core/types/l10n.d.ts +2 -1
  28. package/lib/core/utils/get-ai-prompt-link.d.ts +3 -1
  29. package/lib/core/utils/get-ai-prompt-link.js +1 -0
  30. package/lib/core/utils/mcp.d.ts +1 -0
  31. package/lib/core/utils/mcp.js +4 -0
  32. package/lib/icons/GrokIcon/GrokIcon.d.ts +25 -0
  33. package/lib/icons/GrokIcon/GrokIcon.js +23 -0
  34. package/lib/icons/McpIcon/McpIcon.d.ts +25 -0
  35. package/lib/icons/McpIcon/McpIcon.js +24 -0
  36. package/lib/icons/TranslateIcon/TranslateIcon.d.ts +25 -0
  37. package/lib/icons/TranslateIcon/TranslateIcon.js +21 -0
  38. package/lib/index.d.ts +3 -0
  39. package/lib/index.js +4 -0
  40. package/lib/layouts/CodeWalkthroughLayout.js +9 -1
  41. package/lib/layouts/DocumentationLayout.js +2 -0
  42. package/lib/markdoc/default.d.ts +2 -0
  43. package/lib/markdoc/default.js +2 -0
  44. package/package.json +4 -3
  45. package/src/components/Buttons/MobileMenuButton.tsx +45 -0
  46. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +2 -2
  47. package/src/components/CatalogClassic/CatalogClassicInfoBlock.tsx +1 -1
  48. package/src/components/Feedback/Feedback.tsx +1 -3
  49. package/src/components/Feedback/Mood.tsx +1 -1
  50. package/src/components/Feedback/Rating.tsx +1 -1
  51. package/src/components/Feedback/Scale.tsx +1 -1
  52. package/src/components/Feedback/Sentiment.tsx +1 -1
  53. package/src/components/Filter/Filter.tsx +2 -2
  54. package/src/components/MCP/McpConnectionTabs.tsx +56 -2
  55. package/src/components/MissingTranslationNotice/MissingTranslationNotice.tsx +113 -0
  56. package/src/components/MissingTranslationNotice/variables.dark.ts +7 -0
  57. package/src/components/MissingTranslationNotice/variables.ts +26 -0
  58. package/src/components/Navbar/Navbar.tsx +15 -30
  59. package/src/components/PageActions/PageActions.tsx +1 -0
  60. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +1 -1
  61. package/src/components/Select/Select.tsx +1 -2
  62. package/src/core/hooks/__mocks__/use-theme-hooks.ts +1 -1
  63. package/src/core/hooks/use-active-section-id.ts +13 -0
  64. package/src/core/hooks/use-codeblock-tabs-controls.ts +1 -1
  65. package/src/core/hooks/use-focus-trap.ts +8 -7
  66. package/src/core/hooks/use-page-actions.ts +73 -43
  67. package/src/core/styles/dark.ts +2 -0
  68. package/src/core/styles/global.ts +2 -0
  69. package/src/core/types/hooks.ts +2 -2
  70. package/src/core/types/l10n.ts +15 -2
  71. package/src/core/utils/get-ai-prompt-link.ts +5 -2
  72. package/src/core/utils/mcp.ts +4 -0
  73. package/src/icons/GrokIcon/GrokIcon.tsx +24 -0
  74. package/src/icons/McpIcon/McpIcon.tsx +37 -0
  75. package/src/icons/TranslateIcon/TranslateIcon.tsx +22 -0
  76. package/src/index.ts +4 -0
  77. package/src/layouts/CodeWalkthroughLayout.tsx +11 -1
  78. package/src/layouts/DocumentationLayout.tsx +2 -0
  79. package/src/markdoc/default.ts +2 -0
  80. package/src/markdoc/tags/code-walkthrough.ts +1 -1
@@ -45,6 +45,7 @@ const constants_1 = require("../core/constants");
45
45
  const CodeSnippetContext_1 = require("../core/contexts/CodeSnippetContext");
46
46
  const DocumentationLayoutTop_1 = require("../layouts/DocumentationLayoutTop");
47
47
  const DocumentationLayoutBottom_1 = require("../layouts/DocumentationLayoutBottom");
48
+ const MissingTranslationNotice_1 = require("../components/MissingTranslationNotice/MissingTranslationNotice");
48
49
  function DocumentationLayout({ tableOfContent, feedback, config, editPage, lastModified, nextPage, prevPage, className, children, }) {
49
50
  var _a, _b;
50
51
  const { markdown } = (0, hooks_1.useThemeConfig)();
@@ -53,6 +54,7 @@ function DocumentationLayout({ tableOfContent, feedback, config, editPage, lastM
53
54
  react_1.default.createElement(LayoutWrapper, { "data-component-name": "Layout/DocumentationLayout", className: className, $withToc: withToc },
54
55
  react_1.default.createElement(ContentWrapper, null,
55
56
  react_1.default.createElement(DocumentationLayoutTop_1.DocumentationLayoutTop, { config: config, editPage: editPage, lastModified: lastModified }),
57
+ react_1.default.createElement(MissingTranslationNotice_1.MissingTranslationNotice, null),
56
58
  children,
57
59
  react_1.default.createElement(DocumentationLayoutBottom_1.DocumentationLayoutBottom, { feedback: feedback, nextPage: nextPage, prevPage: prevPage })),
58
60
  tableOfContent)));
@@ -30,6 +30,7 @@ import { icon } from '../markdoc/tags/icon';
30
30
  import { connectMcp } from '../markdoc/tags/connect-mcp';
31
31
  import { loginButton } from '../markdoc/tags/login-button';
32
32
  import { diagram } from '../markdoc/tags/diagram';
33
+ import { visibility } from '../markdoc/tags/visibility';
33
34
  import { prompt } from '../markdoc/tags/prompt';
34
35
  export declare const tags: {
35
36
  [accordion.tagName]: import("./tags/types").MarkdocTagSchema;
@@ -57,5 +58,6 @@ export declare const tags: {
57
58
  [connectMcp.tagName]: import("./tags/types").MarkdocTagSchema;
58
59
  [loginButton.tagName]: import("./tags/types").MarkdocTagSchema;
59
60
  [diagram.tagName]: import("./tags/types").MarkdocTagSchema;
61
+ [visibility.tagName]: import("./tags/types").MarkdocTagSchema;
60
62
  [prompt.tagName]: import("./tags/types").MarkdocTagSchema;
61
63
  };
@@ -72,6 +72,7 @@ const icon_1 = require("../markdoc/tags/icon");
72
72
  const connect_mcp_1 = require("../markdoc/tags/connect-mcp");
73
73
  const login_button_1 = require("../markdoc/tags/login-button");
74
74
  const diagram_1 = require("../markdoc/tags/diagram");
75
+ const visibility_1 = require("../markdoc/tags/visibility");
75
76
  const prompt_1 = require("../markdoc/tags/prompt");
76
77
  exports.tags = {
77
78
  [accordion_1.accordion.tagName]: accordion_1.accordion.schema,
@@ -99,6 +100,7 @@ exports.tags = {
99
100
  [connect_mcp_1.connectMcp.tagName]: connect_mcp_1.connectMcp.schema,
100
101
  [login_button_1.loginButton.tagName]: login_button_1.loginButton.schema,
101
102
  [diagram_1.diagram.tagName]: diagram_1.diagram.schema,
103
+ [visibility_1.visibility.tagName]: visibility_1.visibility.schema,
102
104
  [prompt_1.prompt.tagName]: prompt_1.prompt.schema,
103
105
  };
104
106
  //# sourceMappingURL=default.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.71.0-next.4",
3
+ "version": "0.71.0-next.6",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -43,7 +43,7 @@
43
43
  "devDependencies": {
44
44
  "@babel/core": "7.29.7",
45
45
  "@markdoc/markdoc": "0.5.2",
46
- "@redocly/realm-asyncapi-sdk": "0.16.0",
46
+ "@redocly/realm-asyncapi-sdk": "0.17.0-next.1",
47
47
  "@redocly/tooling-test-utils": "0.0.0",
48
48
  "@shikijs/types": "3.21.0",
49
49
  "@testing-library/react": "16.3.0",
@@ -58,6 +58,7 @@
58
58
  "@vitest/coverage-v8": "4.1.11",
59
59
  "@vitest/ui": "4.1.11",
60
60
  "concurrently": "7.6.0",
61
+ "jest-styled-components": "7.4.0",
61
62
  "react-router": "^8.3.0",
62
63
  "resize-observer-polyfill": "1.5.1",
63
64
  "rimraf": "5.0.7",
@@ -69,7 +70,7 @@
69
70
  "webpack": "5.105.2"
70
71
  },
71
72
  "dependencies": {
72
- "@redocly/config": "0.66.0",
73
+ "@redocly/config": "0.67.0",
73
74
  "@tanstack/react-query": "5.62.3",
74
75
  "@tanstack/react-virtual": "3.13.0",
75
76
  "@xyflow/react": "12.8.3",
@@ -0,0 +1,45 @@
1
+ import React, { useTransition } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+
6
+ import { breakpoints } from '@redocly/theme/core/utils';
7
+ import { Button } from '@redocly/theme/components/Button/Button';
8
+ import { MenuIcon } from '@redocly/theme/icons/MenuIcon/MenuIcon';
9
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
10
+
11
+ export type MobileMenuButtonProps = {
12
+ isOpen: boolean;
13
+ /** Called inside a transition; the button shows the requested state until it commits. */
14
+ onOpenChange: (isOpen: boolean) => void;
15
+ className?: string;
16
+ };
17
+
18
+ export function MobileMenuButton({
19
+ isOpen,
20
+ onOpenChange,
21
+ className,
22
+ }: MobileMenuButtonProps): JSX.Element {
23
+ const [isToggling, startToggle] = useTransition();
24
+ const showClose = isToggling ? !isOpen : isOpen;
25
+
26
+ return (
27
+ <MobileMenuButtonWrapper
28
+ data-component-name="Buttons/MobileMenuButton"
29
+ className={className}
30
+ variant="text"
31
+ data-testid="mobile-menu-button"
32
+ onClick={() => startToggle(() => onOpenChange(!showClose))}
33
+ icon={showClose ? <CloseIcon /> : <MenuIcon />}
34
+ aria-label={showClose ? 'Close menu button' : 'Open menu button'}
35
+ />
36
+ );
37
+ }
38
+
39
+ const MobileMenuButtonWrapper = styled(Button)`
40
+ margin-left: 0px !important;
41
+
42
+ @media screen and (min-width: ${breakpoints.medium}) {
43
+ display: none;
44
+ }
45
+ `;
@@ -2,7 +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 { FilterProps, FilterTypes } from '@redocly/theme/core/types';
5
+ import type { FilterProps } from '@redocly/theme/core/types';
6
6
 
7
7
  import { CatalogFilterSelect } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterSelect';
8
8
  import { CatalogFilterCheckboxes } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterCheckboxes';
@@ -31,7 +31,7 @@ export function CatalogFilter({
31
31
  return null;
32
32
  }
33
33
 
34
- const FilterComponent = filterComponents[(filter.type || 'checkboxes') as FilterTypes];
34
+ const FilterComponent = filterComponents[filter.type || 'checkboxes'];
35
35
 
36
36
  return (
37
37
  <CatalogFilterGroup
@@ -21,7 +21,7 @@ export type CatalogClassicInfoBlockProps = {
21
21
  export function CatalogClassicInfoBlock(props: CatalogClassicInfoBlockProps): JSX.Element {
22
22
  const scorecardBadge = props.metadata?.scorecardLevel ? (
23
23
  <ScorecardBadge
24
- level={props.metadata.scorecardLevel as string}
24
+ level={props.metadata.scorecardLevel}
25
25
  slug={props.metadata?.scoreCardSlug as string}
26
26
  colorVariable={getScorecardColorVariable(
27
27
  props.metadata?.scorecardLevelIdx || 0,
@@ -43,9 +43,7 @@ export function Feedback(props: FeedbackProps & { path?: string }) {
43
43
  const renderFeedbackComponent = () => {
44
44
  const { type, settings, path } = feedbackConf;
45
45
 
46
- const FeedbackComponent = feedbackComponents[
47
- type as FeedbackComponentsKeys
48
- ] as FeedbackComponents[FeedbackComponentsKeys];
46
+ const FeedbackComponent = feedbackComponents[type as FeedbackComponentsKeys];
49
47
 
50
48
  if (!FeedbackComponent) {
51
49
  return null;
@@ -134,8 +134,8 @@ export function Mood({ settings, onSubmit, className }: MoodProps): JSX.Element
134
134
 
135
135
  const displayReasons = checkIfShouldDisplayReasons(score);
136
136
  const displayComment = !!score && hasComment;
137
- const displaySubmitBnt = !!(score && (displayReasons || displayComment));
138
137
  const displayFeedbackEmail = !!score && hasOptionalEmail;
138
+ const displaySubmitBnt = !!score && (displayReasons || displayComment || displayFeedbackEmail);
139
139
 
140
140
  const submitMood = (moodScore: string) => {
141
141
  const trimmedComment = comment?.trim() || undefined;
@@ -101,8 +101,8 @@ export function Rating({ settings, onSubmit, className }: RatingProps): JSX.Elem
101
101
 
102
102
  const displayReasons = !!score && hasReasons;
103
103
  const displayComment = !!score && hasComment;
104
- const displaySubmitBnt = !!(score && (displayReasons || displayComment));
105
104
  const displayFeedbackEmail = !!score && hasOptionalEmail;
105
+ const displaySubmitBnt = !!score && (displayReasons || displayComment || displayFeedbackEmail);
106
106
 
107
107
  if (isSubmitted) {
108
108
  return (
@@ -121,8 +121,8 @@ export function Scale({ settings, onSubmit, className }: ScaleProps): JSX.Elemen
121
121
 
122
122
  const displayReasons = !!score && hasReasons;
123
123
  const displayComment = !!score && hasComment;
124
- const displaySubmitBnt = !!score && (displayReasons || displayComment);
125
124
  const displayFeedbackEmail = !!score && hasOptionalEmail;
125
+ const displaySubmitBnt = !!score && (displayReasons || displayComment || displayFeedbackEmail);
126
126
 
127
127
  if (isSubmitted) {
128
128
  return (
@@ -90,8 +90,8 @@ export function Sentiment({ settings, onSubmit, className }: SentimentProps): JS
90
90
 
91
91
  const displayReasons = checkIfShouldDisplayReasons(score);
92
92
  const displayComment = !!score && hasComment;
93
- const displaySubmitBnt = !!(score && (displayReasons || displayComment));
94
93
  const displayFeedbackEmail = !!score && hasOptionalEmail;
94
+ const displaySubmitBnt = !!score && (displayReasons || displayComment || displayFeedbackEmail);
95
95
 
96
96
  const commentLabel =
97
97
  score === 1
@@ -2,7 +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 { FilterProps, FilterTypes } from '@redocly/theme/core/types';
5
+ import type { FilterProps } from '@redocly/theme/core/types';
6
6
 
7
7
  import { FilterSelect } from '@redocly/theme/components/Filter/FilterSelect';
8
8
  import { FilterCheckboxes } from '@redocly/theme/components/Filter/FilterCheckboxes';
@@ -21,7 +21,7 @@ export function Filter({
21
21
  }: FilterProps): JSX.Element | null {
22
22
  if (!filter.parentUsed) return null;
23
23
 
24
- const FilterComponent = filterComponents[(filter.type || 'checkboxes') as FilterTypes];
24
+ const FilterComponent = filterComponents[filter.type || 'checkboxes'];
25
25
 
26
26
  return (
27
27
  <FilterGroup data-component-name="Filter/Filter" key={filter.property + filter.title}>
@@ -10,6 +10,7 @@ import { useMCPConfig, useThemeHooks } from '@redocly/theme/core/hooks';
10
10
  import {
11
11
  generateClaudeCodeCommand,
12
12
  generateCodexCommand,
13
+ generateGrokBuildCommand,
13
14
  generateMCPDeepLink,
14
15
  generateMcpInstallPrompt,
15
16
  getBaseDataAttributes,
@@ -18,19 +19,20 @@ import { Button } from '@redocly/theme/components/Button/Button';
18
19
  import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
19
20
  import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
20
21
  import { ChatGptIcon } from '@redocly/theme/icons/ChatGptIcon/ChatGptIcon';
22
+ import { GrokIcon } from '@redocly/theme/icons/GrokIcon/GrokIcon';
21
23
  import { Tabs, TabsSize } from '@redocly/theme/markdoc/components/Tabs/Tabs';
22
24
  import { TabItemFragment } from '@redocly/theme/markdoc/components/Tabs/TabItemFragment';
23
25
  import { NumberedList } from '@redocly/theme/markdoc/components/NumberedList/NumberedList';
24
26
  import { NumberedItem } from '@redocly/theme/markdoc/components/NumberedList/NumberedItem';
25
27
 
26
- export type McpConnectionTab = 'claude' | 'chatgpt' | 'json';
28
+ export type McpConnectionTab = 'claude' | 'chatgpt' | 'grok' | 'json';
27
29
 
28
30
  export type McpConnectionTabsProps = {
29
31
  options?: McpConnectionTab[];
30
32
  className?: string;
31
33
  };
32
34
 
33
- const DEFAULT_TAB_OPTIONS: McpConnectionTab[] = ['claude', 'chatgpt', 'json'];
35
+ const DEFAULT_TAB_OPTIONS: McpConnectionTab[] = ['claude', 'chatgpt', 'grok', 'json'];
34
36
 
35
37
  const TAB_LABELS: Record<
36
38
  McpConnectionTab,
@@ -46,9 +48,16 @@ const TAB_LABELS: Record<
46
48
  defaultLabel: 'ChatGPT',
47
49
  icon: ChatGptIcon,
48
50
  },
51
+ grok: {
52
+ translationKey: 'page.mcpNotAllowed.tabs.grok',
53
+ defaultLabel: 'Grok',
54
+ icon: GrokIcon,
55
+ },
49
56
  json: { translationKey: 'page.mcpNotAllowed.tabs.json', defaultLabel: 'JSON' },
50
57
  };
51
58
 
59
+ const GROK_CONNECTORS_URL = 'https://grok.com/connectors';
60
+
52
61
  type TabClient = { headline: string; content: ReactNode };
53
62
 
54
63
  type SnippetId =
@@ -235,6 +244,51 @@ function renderTabContent(
235
244
  },
236
245
  assistantPromptClient,
237
246
  ]);
247
+ case 'grok':
248
+ return renderClientList([
249
+ {
250
+ headline: translate('page.mcpNotAllowed.snippets.grokBuild.headline', 'Grok Build'),
251
+ content: (
252
+ <>
253
+ {terminalHint}
254
+ <CodeBlock
255
+ lang="shell"
256
+ source={generateGrokBuildCommand(connection)}
257
+ header={snippetHeader(option, 'grok-build')}
258
+ wrapContents
259
+ />
260
+ </>
261
+ ),
262
+ },
263
+ {
264
+ headline: translate('page.mcpNotAllowed.snippets.grokApp.headline', 'Grok app'),
265
+ content: (
266
+ <>
267
+ <ConnectButton
268
+ variant="secondary"
269
+ icon={<GrokIcon />}
270
+ onClick={() => window.open(GROK_CONNECTORS_URL, '_blank')}
271
+ >
272
+ {translate(
273
+ 'page.mcpNotAllowed.snippets.grokApp.openConnectors',
274
+ 'Open Grok connectors',
275
+ )}
276
+ </ConnectButton>
277
+ <TabHint>
278
+ {translate(
279
+ 'page.mcpNotAllowed.snippets.grokApp.instruction',
280
+ 'On grok.com, open Connectors → New Connector → Custom and paste this server URL:',
281
+ )}
282
+ </TabHint>
283
+ <CodeBlock
284
+ lang="text"
285
+ source={connection.url}
286
+ header={snippetHeader(option, 'grok-app')}
287
+ />
288
+ </>
289
+ ),
290
+ },
291
+ ]);
238
292
  case 'json':
239
293
  return (
240
294
  <>
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+
6
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
7
+ import { TranslateIcon } from '@redocly/theme/icons/TranslateIcon/TranslateIcon';
8
+ import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
9
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
10
+
11
+ export type MissingTranslationNoticeProps = {
12
+ className?: string;
13
+ };
14
+
15
+ /**
16
+ * Tells the reader that the page shows the original language,
17
+ * because the translation for the selected locale is missing.
18
+ * Renders only on fallback pages of a locale that sets `showMissingTranslationNotice`.
19
+ * The reader can close the notice for the current page.
20
+ */
21
+ export function MissingTranslationNotice({
22
+ className,
23
+ }: MissingTranslationNoticeProps): JSX.Element | null {
24
+ const { useTranslate, useMarkdownText, usePageProps, useL10nConfig } = useThemeHooks();
25
+ const { translate } = useTranslate();
26
+ const { isTranslationFallback } = usePageProps<{ isTranslationFallback?: boolean }>();
27
+ const { currentLocale, defaultLocale, locales } = useL10nConfig();
28
+ const [isDismissed, setIsDismissed] = React.useState(false);
29
+
30
+ const isShown =
31
+ isTranslationFallback === true &&
32
+ currentLocale !== defaultLocale &&
33
+ locales.some(
34
+ ({ code, showMissingTranslationNotice }) =>
35
+ code === currentLocale && showMissingTranslationNotice === true,
36
+ );
37
+ const language = locales.find(({ code }) => code === defaultLocale)?.name || defaultLocale;
38
+ const content = useMarkdownText(
39
+ isShown ? translate('missingTranslationNotice.content', { language }) : '',
40
+ );
41
+
42
+ if (!isShown || isDismissed) {
43
+ return null;
44
+ }
45
+
46
+ return (
47
+ <MissingTranslationNoticeWrapper
48
+ className={className}
49
+ data-component-name="MissingTranslationNotice/MissingTranslationNotice"
50
+ >
51
+ <Icon color="--missing-translation-notice-icon-color" />
52
+ <Content>
53
+ <Title>
54
+ {translate('missingTranslationNotice.title', {
55
+ defaultValue: 'This page has not yet been translated',
56
+ })}
57
+ </Title>
58
+ <Markdown compact>{content}</Markdown>
59
+ </Content>
60
+ <CloseButton
61
+ type="button"
62
+ aria-label={translate('missingTranslationNotice.close', { defaultValue: 'Close' })}
63
+ onClick={() => setIsDismissed(true)}
64
+ >
65
+ <CloseIcon color="--missing-translation-notice-text-color" />
66
+ </CloseButton>
67
+ </MissingTranslationNoticeWrapper>
68
+ );
69
+ }
70
+
71
+ const MissingTranslationNoticeWrapper = styled.div`
72
+ display: flex;
73
+ flex-direction: row;
74
+ align-items: flex-start;
75
+ gap: var(--missing-translation-notice-gap);
76
+
77
+ padding: var(--missing-translation-notice-padding);
78
+ margin: var(--missing-translation-notice-margin-vertical) 0;
79
+ color: var(--missing-translation-notice-text-color);
80
+ background-color: var(--missing-translation-notice-bg-color);
81
+ border: 1px solid var(--missing-translation-notice-border-color);
82
+ border-left: var(--missing-translation-notice-accent-width) solid
83
+ var(--missing-translation-notice-accent-color);
84
+ border-radius: var(--missing-translation-notice-border-radius);
85
+ `;
86
+
87
+ const Icon = styled(TranslateIcon)`
88
+ flex-shrink: 0;
89
+ `;
90
+
91
+ const Content = styled.div`
92
+ display: flex;
93
+ flex-direction: column;
94
+ flex: 1;
95
+ gap: var(--spacing-xxs);
96
+
97
+ font-size: var(--missing-translation-notice-font-size);
98
+ line-height: var(--missing-translation-notice-line-height);
99
+ `;
100
+
101
+ const Title = styled.span`
102
+ font-size: var(--missing-translation-notice-title-font-size);
103
+ font-weight: var(--missing-translation-notice-title-font-weight);
104
+ `;
105
+
106
+ const CloseButton = styled.button`
107
+ display: flex;
108
+ flex-shrink: 0;
109
+ padding: 0;
110
+ border: none;
111
+ background: none;
112
+ cursor: pointer;
113
+ `;
@@ -0,0 +1,7 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const missingTranslationNoticeDarkMode = css`
4
+ --missing-translation-notice-bg-color: var(--color-warm-grey-3); // @presenter Color
5
+ --missing-translation-notice-icon-color: var(--color-blue-4); // @presenter Color
6
+ --missing-translation-notice-accent-color: var(--color-blue-4); // @presenter Color
7
+ `;
@@ -0,0 +1,26 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const missingTranslationNotice = css`
4
+ /**
5
+ * @tokens Missing translation notice
6
+ */
7
+
8
+ --missing-translation-notice-bg-color: var(--color-warm-grey-1); // @presenter Color
9
+ --missing-translation-notice-text-color: var(--text-color-primary); // @presenter Color
10
+ --missing-translation-notice-icon-color: var(--color-blue-6); // @presenter Color
11
+ --missing-translation-notice-border-color: var(--border-color-secondary); // @presenter Color
12
+ --missing-translation-notice-accent-color: var(--color-blue-6); // @presenter Color
13
+
14
+ --missing-translation-notice-accent-width: 3px; // @presenter Spacing
15
+ --missing-translation-notice-border-radius: var(--border-radius-lg); // @presenter BorderRadius
16
+ --missing-translation-notice-padding: var(--spacing-sm) var(--spacing-base); // @presenter Spacing
17
+ --missing-translation-notice-margin-vertical: var(--spacing-base); // @presenter Spacing
18
+ --missing-translation-notice-gap: var(--spacing-sm); // @presenter Spacing
19
+
20
+ --missing-translation-notice-title-font-size: var(--font-size-base); // @presenter FontSize
21
+ --missing-translation-notice-title-font-weight: var(--font-weight-semibold); // @presenter FontWeight
22
+ --missing-translation-notice-font-size: var(--font-size-base); // @presenter FontSize
23
+ --missing-translation-notice-line-height: var(--line-height-base); // @presenter LineHeight
24
+
25
+ // @tokens End
26
+ `;
@@ -19,9 +19,7 @@ import { Search } from '@redocly/theme/components/Search/Search';
19
19
  import { UserMenu } from '@redocly/theme/components/UserMenu/UserMenu';
20
20
  import { LanguagePicker } from '@redocly/theme/components/LanguagePicker/LanguagePicker';
21
21
  import { ProductPicker } from '@redocly/theme/components/Product/ProductPicker';
22
- import { Button } from '@redocly/theme/components/Button/Button';
23
- import { MenuIcon } from '@redocly/theme/icons/MenuIcon/MenuIcon';
24
- import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
22
+ import { MobileMenuButton } from '@redocly/theme/components/Buttons/MobileMenuButton';
25
23
  import { Banner } from '@redocly/theme/components/Banner/Banner';
26
24
 
27
25
  export type NavbarProps = {
@@ -69,25 +67,20 @@ export function Navbar({ className }: NavbarProps): JSX.Element | null {
69
67
  <LanguagePicker onChangeLanguage={changeLanguage} onlyIcon alignment="end" />
70
68
  <ColorModeSwitcher />
71
69
  <MobileMenuButton
72
- variant="text"
73
- data-testid="mobile-menu-button"
74
- onClick={
75
- isOpen
76
- ? () => {
77
- closeMobileMenu();
78
- telemetry.sendMobileMenuClosedMessage([
79
- getBaseDataAttributes('mobileMenuClose', 'button'),
80
- ]);
81
- }
82
- : () => {
83
- openMobileMenu();
84
- telemetry.sendMobileMenuOpenedMessage([
85
- getBaseDataAttributes('mobileMenuOpen', 'button'),
86
- ]);
87
- }
88
- }
89
- icon={isOpen ? <CloseIcon /> : <MenuIcon />}
90
- aria-label={isOpen ? 'Close menu button' : 'Open menu button'}
70
+ isOpen={isOpen}
71
+ onOpenChange={(open) => {
72
+ if (open) {
73
+ openMobileMenu();
74
+ telemetry.sendMobileMenuOpenedMessage([
75
+ getBaseDataAttributes('mobileMenuOpen', 'button'),
76
+ ]);
77
+ } else {
78
+ closeMobileMenu();
79
+ telemetry.sendMobileMenuClosedMessage([
80
+ getBaseDataAttributes('mobileMenuClose', 'button'),
81
+ ]);
82
+ }
83
+ }}
91
84
  />
92
85
  {hideUserMenu ? null : <UserMenu />}
93
86
  </NavbarRow>
@@ -147,11 +140,3 @@ const NavbarRow = styled.div`
147
140
  margin-right: auto;
148
141
  }
149
142
  `;
150
-
151
- const MobileMenuButton = styled(Button)`
152
- margin-left: 0px !important;
153
-
154
- @media screen and (min-width: ${breakpoints.medium}) {
155
- display: none;
156
- }
157
- `;
@@ -112,6 +112,7 @@ function renderIcon(buttonAction: PageAction, actionState: ActionState) {
112
112
  const PageActionsWrapper = styled.div`
113
113
  margin-left: auto;
114
114
  padding-left: var(--page-actions-padding-left);
115
+ user-select: none;
115
116
  --button-color: var(--page-actions-button-text-color);
116
117
 
117
118
  .button-group-size-medium .button.button-size-medium {
@@ -92,7 +92,7 @@ function facetValueToSelectOption(facetValue?: SearchFacetCount | string) {
92
92
  value: facetValue,
93
93
  };
94
94
  } else {
95
- const { value, count, isCounterVisible } = facetValue as SearchFacetCount;
95
+ const { value, count, isCounterVisible } = facetValue;
96
96
  return {
97
97
  element: (
98
98
  <FilterSelectOptionWrapper>
@@ -46,8 +46,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
46
46
  .map((value) => {
47
47
  const selectedOption = options.find((option) => option.value === value);
48
48
  return (
49
- selectedOption ||
50
- (typeof value === 'string' ? ({ value } as SelectOption<T>) : (value as SelectOption<T>))
49
+ selectedOption || (typeof value === 'string' ? { value } : (value as SelectOption<T>))
51
50
  );
52
51
  })
53
52
  .filter((option) => !!option);
@@ -7,7 +7,7 @@ export const useThemeHooks = vi.fn(() => ({
7
7
  translate: vi.fn((key: string, defaultValue: string | { defaultValue: string }) =>
8
8
  defaultValue
9
9
  ? typeof defaultValue !== 'string'
10
- ? (defaultValue as { defaultValue: string }).defaultValue
10
+ ? defaultValue.defaultValue
11
11
  : defaultValue
12
12
  : key,
13
13
  ),
@@ -60,7 +60,15 @@ export function useActiveSectionId(
60
60
  return;
61
61
  }
62
62
 
63
+ // A one-event scroll jump is read before the section it lands on stops being skipped.
64
+ const onContentVisibilityChange = (event: Event) => {
65
+ if ('skipped' in event && event.skipped === false) {
66
+ scrollListener();
67
+ }
68
+ };
69
+
63
70
  window.addEventListener('scroll', scrollListener, { capture: false });
71
+ window.addEventListener('contentvisibilityautostatechange', onContentVisibilityChange, true);
64
72
  const initialRead = setTimeout(() => {
65
73
  scrollListener();
66
74
  }, 10);
@@ -69,6 +77,11 @@ export function useActiveSectionId(
69
77
  clearTimeout(initialRead);
70
78
  scrollListener.cancel();
71
79
  window.removeEventListener('scroll', scrollListener);
80
+ window.removeEventListener(
81
+ 'contentvisibilityautostatechange',
82
+ onContentVisibilityChange,
83
+ true,
84
+ );
72
85
  };
73
86
  }, [location, heightOffset, scrollListener]);
74
87
 
@@ -41,7 +41,7 @@ export function useCodeBlockTabsControls({ tabs, containerRef, tabRefs }: CodeBl
41
41
  resizeObserver.observe(container);
42
42
 
43
43
  return () => {
44
- resizeObserver.unobserve(container as HTMLDivElement);
44
+ resizeObserver.unobserve(container);
45
45
  };
46
46
  }
47
47
  // eslint-disable-next-line react-hooks/exhaustive-deps