@redocly/theme 0.60.0-next.6 → 0.60.0-next.8

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 (46) hide show
  1. package/lib/components/Banner/Banner.d.ts +6 -0
  2. package/lib/components/Banner/Banner.js +172 -0
  3. package/lib/components/Banner/variables.d.ts +1 -0
  4. package/lib/components/Banner/variables.dark.d.ts +1 -0
  5. package/lib/components/Banner/variables.dark.js +12 -0
  6. package/lib/components/Banner/variables.js +45 -0
  7. package/lib/components/Buttons/AIAssistantButton.js +2 -1
  8. package/lib/components/LanguagePicker/LanguagePicker.js +1 -0
  9. package/lib/components/Navbar/Navbar.js +13 -5
  10. package/lib/components/Search/Search.js +6 -1
  11. package/lib/components/Search/SearchDialog.js +11 -5
  12. package/lib/core/contexts/SearchContext.d.ts +10 -0
  13. package/lib/core/contexts/SearchContext.js +56 -0
  14. package/lib/core/contexts/index.d.ts +1 -0
  15. package/lib/core/contexts/index.js +1 -0
  16. package/lib/core/hooks/search/use-search-dialog.js +4 -1
  17. package/lib/core/hooks/use-telemetry-fallback.d.ts +2 -0
  18. package/lib/core/hooks/use-telemetry-fallback.js +2 -0
  19. package/lib/core/hooks/use-theme-hooks.js +1 -0
  20. package/lib/core/openapi/index.d.ts +1 -0
  21. package/lib/core/openapi/index.js +4 -1
  22. package/lib/core/styles/dark.js +2 -0
  23. package/lib/core/styles/global.js +32 -30
  24. package/lib/core/types/hooks.d.ts +7 -3
  25. package/lib/index.d.ts +1 -0
  26. package/lib/index.js +2 -0
  27. package/package.json +7 -7
  28. package/src/components/Banner/Banner.tsx +179 -0
  29. package/src/components/Banner/variables.dark.ts +10 -0
  30. package/src/components/Banner/variables.ts +43 -0
  31. package/src/components/Buttons/AIAssistantButton.tsx +3 -2
  32. package/src/components/LanguagePicker/LanguagePicker.tsx +1 -0
  33. package/src/components/Navbar/Navbar.tsx +13 -5
  34. package/src/components/Search/Search.tsx +10 -1
  35. package/src/components/Search/SearchDialog.tsx +12 -5
  36. package/src/core/contexts/SearchContext.tsx +31 -0
  37. package/src/core/contexts/index.ts +1 -0
  38. package/src/core/hooks/__mocks__/use-theme-hooks.ts +4 -0
  39. package/src/core/hooks/search/use-search-dialog.ts +4 -1
  40. package/src/core/hooks/use-telemetry-fallback.ts +2 -0
  41. package/src/core/hooks/use-theme-hooks.ts +1 -0
  42. package/src/core/openapi/index.ts +1 -0
  43. package/src/core/styles/dark.ts +2 -0
  44. package/src/core/styles/global.ts +2 -0
  45. package/src/core/types/hooks.ts +6 -5
  46. package/src/index.ts +2 -0
@@ -28,21 +28,22 @@ const variables_23 = require("../../components/Menu/variables");
28
28
  const variables_24 = require("../../components/CodeBlock/variables");
29
29
  const variables_25 = require("../../components/Product/variables");
30
30
  const variables_26 = require("../../components/Markdown/variables");
31
- const variables_27 = require("../../markdoc/components/Tabs/variables");
32
- const variables_28 = require("../../markdoc/components/Mermaid/variables");
33
- const variables_29 = require("../../components/LastUpdated/variables");
34
- const variables_30 = require("../../components/Logo/variables");
35
- const variables_31 = require("../../components/StatusCode/variables");
36
- const variables_32 = require("../../components/Segmented/variables");
37
- const variables_33 = require("../../components/UserMenu/variables");
38
- const variables_34 = require("../../components/Tags/variables");
39
- const variables_35 = require("../../components/VersionPicker/variables");
40
- const variables_36 = require("../../components/DatePicker/variables");
41
- const variables_37 = require("../../components/Switch/variables");
42
- const variables_38 = require("../../markdoc/components/Cards/variables");
43
- const variables_39 = require("../../markdoc/components/CodeWalkthrough/variables");
44
- const variables_40 = require("../../components/SkipContent/variables");
45
- const variables_41 = require("../../components/PageActions/variables");
31
+ const variables_27 = require("../../components/Banner/variables");
32
+ const variables_28 = require("../../markdoc/components/Tabs/variables");
33
+ const variables_29 = require("../../markdoc/components/Mermaid/variables");
34
+ const variables_30 = require("../../components/LastUpdated/variables");
35
+ const variables_31 = require("../../components/Logo/variables");
36
+ const variables_32 = require("../../components/StatusCode/variables");
37
+ const variables_33 = require("../../components/Segmented/variables");
38
+ const variables_34 = require("../../components/UserMenu/variables");
39
+ const variables_35 = require("../../components/Tags/variables");
40
+ const variables_36 = require("../../components/VersionPicker/variables");
41
+ const variables_37 = require("../../components/DatePicker/variables");
42
+ const variables_38 = require("../../components/Switch/variables");
43
+ const variables_39 = require("../../markdoc/components/Cards/variables");
44
+ const variables_40 = require("../../markdoc/components/CodeWalkthrough/variables");
45
+ const variables_41 = require("../../components/SkipContent/variables");
46
+ const variables_42 = require("../../components/PageActions/variables");
46
47
  const dark_1 = require("./dark");
47
48
  const themeColors = (0, styled_components_1.css) `
48
49
  /* === Palette === */
@@ -1216,32 +1217,33 @@ exports.styles = (0, styled_components_1.css) `
1216
1217
  ${apiReferenceDocs}
1217
1218
  ${variables_11.apiReferencePanels}
1218
1219
  ${badges}
1220
+ ${variables_27.banner}
1219
1221
  ${borders}
1220
1222
  ${variables_5.breadcrumbs}
1221
1223
  ${variables_19.button}
1222
1224
  ${variables_20.aiAssistantButton}
1223
1225
  ${variables_20.connectMCPButton}
1224
- ${variables_38.cards}
1226
+ ${variables_39.cards}
1225
1227
  ${variables_8.catalog}
1226
1228
  ${variables_10.catalogClassic}
1227
1229
  ${variables_24.code}
1228
- ${variables_39.codeWalkthrough}
1230
+ ${variables_40.codeWalkthrough}
1229
1231
  ${docsDropdown}
1230
1232
  ${variables_14.dropdown}
1231
1233
  ${error}
1232
1234
  ${variables_9.filter}
1233
1235
  ${variables_18.footer}
1234
1236
  ${headingsTypography}
1235
- ${variables_34.httpTag}
1237
+ ${variables_35.httpTag}
1236
1238
  ${inputs}
1237
1239
  ${variables_1.languagePicker}
1238
- ${variables_29.lastUpdated}
1240
+ ${variables_30.lastUpdated}
1239
1241
  ${links}
1240
1242
  ${loadProgressBar}
1241
- ${variables_30.logo}
1243
+ ${variables_31.logo}
1242
1244
  ${variables_26.markdown}
1243
- ${variables_27.markdownTabs}
1244
- ${variables_28.mermaid}
1245
+ ${variables_28.markdownTabs}
1246
+ ${variables_29.mermaid}
1245
1247
  ${variables_23.menu}
1246
1248
  ${variables_23.mobileMenu}
1247
1249
  ${modal}
@@ -1259,23 +1261,23 @@ exports.styles = (0, styled_components_1.css) `
1259
1261
  ${variables_7.toc}
1260
1262
  ${variables_15.tooltip}
1261
1263
  ${typography}
1262
- ${variables_33.userMenu}
1263
- ${variables_35.versionPicker}
1264
+ ${variables_34.userMenu}
1265
+ ${variables_36.versionPicker}
1264
1266
  ${zIndexDepth}
1265
1267
  ${scorecardColors}
1266
- ${variables_31.statusCode}
1268
+ ${variables_32.statusCode}
1267
1269
  ${tab}
1268
1270
  ${icon}
1269
1271
  ${tree}
1270
- ${variables_32.segmented}
1271
- ${variables_37.switcher}
1272
+ ${variables_33.segmented}
1273
+ ${variables_38.switcher}
1272
1274
  ${variables_16.checkbox}
1273
1275
  ${variables_3.feedback}
1274
1276
  ${variables_2.scorecard}
1275
- ${variables_36.datePicker}
1277
+ ${variables_37.datePicker}
1276
1278
  ${replay}
1277
- ${variables_40.skipContent}
1278
- ${variables_41.pageActions}
1279
+ ${variables_41.skipContent}
1280
+ ${variables_42.pageActions}
1279
1281
 
1280
1282
  background-color: var(--bg-color);
1281
1283
  color: var(--text-color-primary);
@@ -1,5 +1,5 @@
1
1
  import type { AsyncApiRealmUI } from '@redocly/realm-asyncapi-sdk';
2
- import type { CatalogEntityConfig, PageData, PageProps, ResolvedNavItemWithLink, Version } from '@redocly/config';
2
+ import type { BannerConfig, CatalogEntityConfig, PageData, PageProps, ResolvedNavItemWithLink, Version } from '@redocly/config';
3
3
  import type { ShikiTransformer } from '@shikijs/types';
4
4
  import type { Callback, TFunction as TFunc } from 'i18next';
5
5
  import type { To, Location, NavigateFunction } from 'react-router-dom';
@@ -60,7 +60,11 @@ export type ThemeHooks = {
60
60
  breadcrumbs: BreadcrumbItem[];
61
61
  currentItemSiblings?: BreadcrumbItem[];
62
62
  };
63
- useSearch: (product?: string, autoSearchDisabled?: boolean, searchSessionId?: string) => {
63
+ useBanner: () => {
64
+ banner: BannerConfig | undefined;
65
+ dismissBanner: (content: string) => void;
66
+ };
67
+ useSearch: (product?: string, autoSearchDisabled?: boolean) => {
64
68
  query: string;
65
69
  setQuery: React.Dispatch<React.SetStateAction<string>>;
66
70
  filter: SearchFilterItem[];
@@ -78,7 +82,7 @@ export type ThemeHooks = {
78
82
  };
79
83
  useAiSearch: (options?: {
80
84
  filter?: SearchFilterItem[];
81
- }, searchSessionId?: string) => {
85
+ }) => {
82
86
  askQuestion: (question: string, history?: AiSearchConversationItem[]) => void;
83
87
  isGeneratingResponse: boolean;
84
88
  question: string;
package/lib/index.d.ts CHANGED
@@ -44,6 +44,7 @@ export * from './components/Footer/Footer';
44
44
  export * from './components/Footer/FooterColumn';
45
45
  export * from './components/Footer/FooterCopyright';
46
46
  export * from './components/Footer/FooterItem';
47
+ export * from './components/Banner/Banner';
47
48
  export * from './components/Typography/CompactTypography';
48
49
  export * from './components/Typography/Emphasis';
49
50
  export * from './components/Typography/H1';
package/lib/index.js CHANGED
@@ -89,6 +89,8 @@ __exportStar(require("./components/Footer/Footer"), exports);
89
89
  __exportStar(require("./components/Footer/FooterColumn"), exports);
90
90
  __exportStar(require("./components/Footer/FooterCopyright"), exports);
91
91
  __exportStar(require("./components/Footer/FooterItem"), exports);
92
+ /* Banner */
93
+ __exportStar(require("./components/Banner/Banner"), exports);
92
94
  /* Typography */
93
95
  __exportStar(require("./components/Typography/CompactTypography"), exports);
94
96
  __exportStar(require("./components/Typography/Emphasis"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.60.0-next.6",
3
+ "version": "0.60.0-next.8",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -29,8 +29,8 @@
29
29
  "@markdoc/markdoc": "0.5.2",
30
30
  "lodash.debounce": "^4.0.8",
31
31
  "lodash.throttle": "^4.1.1",
32
- "react": "^19.1.0",
33
- "react-dom": "^19.1.0",
32
+ "react": "^19.2.1",
33
+ "react-dom": "^19.2.1",
34
34
  "react-router-dom": "^6.21.1",
35
35
  "styled-components": "^4.1.1 || ^5.3.11 || ^6.0.0"
36
36
  },
@@ -45,8 +45,8 @@
45
45
  "@types/lodash.throttle": "4.1.9",
46
46
  "@types/node": "22.18.13",
47
47
  "@types/nprogress": "0.2.3",
48
- "@types/react": "^19.1.4",
49
- "@types/react-dom": "^19.1.4",
48
+ "@types/react": "^19.2.7",
49
+ "@types/react-dom": "^19.2.3",
50
50
  "@types/styled-components": "5.1.34",
51
51
  "@vitest/coverage-v8": "^4.0.10",
52
52
  "@vitest/ui": "3.2.4",
@@ -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.6.0-next.1"
66
+ "@redocly/realm-asyncapi-sdk": "0.6.0-next.2"
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.40.0"
84
+ "@redocly/config": "0.41.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\")",
@@ -0,0 +1,179 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+ import styled, { css } from 'styled-components';
3
+
4
+ import type { BannerConfig } from '@redocly/config';
5
+ import type { JSX } from 'react';
6
+
7
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
8
+ import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
9
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
10
+ import { Button } from '@redocly/theme/components/Button/Button';
11
+
12
+ type BannerColor = 'info' | 'success' | 'warning' | 'error';
13
+
14
+ type BannerProps = {
15
+ className?: string;
16
+ };
17
+
18
+ function setBannerHeight(height: number): void {
19
+ document.documentElement.style.setProperty('--banner-height', `${height}px`);
20
+ }
21
+
22
+ export function Banner({ className }: BannerProps): JSX.Element | null {
23
+ const { useBanner, useMarkdownText } = useThemeHooks();
24
+ const { banner, dismissBanner } = useBanner();
25
+ const [displayBanner, setDisplayBanner] = useState<BannerConfig | undefined>(undefined);
26
+ const [isVisible, setIsVisible] = useState(false);
27
+ const markdownContent = useMarkdownText(displayBanner?.content || '');
28
+ const bannerRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ if (banner) {
32
+ setDisplayBanner(banner);
33
+ requestAnimationFrame(() => {
34
+ requestAnimationFrame(() => {
35
+ setIsVisible(true);
36
+ });
37
+ });
38
+ } else {
39
+ setIsVisible(false);
40
+ const timer = setTimeout(() => {
41
+ setDisplayBanner(undefined);
42
+ }, 400);
43
+ return () => clearTimeout(timer);
44
+ }
45
+ }, [banner]);
46
+
47
+ useEffect(() => {
48
+ if (!displayBanner) {
49
+ const timer = setTimeout(() => {
50
+ setBannerHeight(0);
51
+ }, 400);
52
+ return () => clearTimeout(timer);
53
+ }
54
+
55
+ const bannerElement = bannerRef.current;
56
+ if (!bannerElement) return;
57
+
58
+ if (!isVisible) {
59
+ setBannerHeight(0);
60
+ return;
61
+ }
62
+
63
+ const updateHeight = (): void => {
64
+ const height = bannerElement.getBoundingClientRect().height;
65
+ setBannerHeight(height);
66
+ };
67
+
68
+ updateHeight();
69
+
70
+ const resizeObserver = new ResizeObserver(updateHeight);
71
+ resizeObserver.observe(bannerElement);
72
+
73
+ return () => {
74
+ resizeObserver.disconnect();
75
+ };
76
+ }, [displayBanner, isVisible]);
77
+
78
+ if (!displayBanner) {
79
+ return null;
80
+ }
81
+
82
+ const bannerColor =
83
+ ((displayBanner as BannerConfig & { color?: BannerColor }).color as BannerColor) || 'info';
84
+
85
+ return (
86
+ <BannerWrapper
87
+ ref={bannerRef}
88
+ data-component-name="Banner/Banner"
89
+ className={className}
90
+ $color={bannerColor}
91
+ $isVisible={isVisible}
92
+ >
93
+ <BannerContent>
94
+ <Markdown compact>{markdownContent}</Markdown>
95
+ </BannerContent>
96
+ {displayBanner.dismissible && (
97
+ <DismissButton
98
+ variant="ghost"
99
+ size="var(--banner-button-size)"
100
+ icon={<CloseIcon color={`var(--banner-${bannerColor}-icon-color)`} size="16px" />}
101
+ onClick={() => dismissBanner(displayBanner.content)}
102
+ aria-label="Dismiss banner"
103
+ />
104
+ )}
105
+ </BannerWrapper>
106
+ );
107
+ }
108
+
109
+ const BannerContent = styled.div`
110
+ flex: 1;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+
115
+ p {
116
+ margin: 0;
117
+ color: var(--banner-text-color);
118
+ text-align: center;
119
+
120
+ a:not([role='button']) {
121
+ color: var(--banner-link-color);
122
+ text-decoration: var(--banner-link-decoration);
123
+ }
124
+
125
+ [data-component-name='Button/Button'] {
126
+ --button-font-size: var(--banner-button-font-size);
127
+ --button-border-radius: var(--banner-button-border-radius);
128
+ --button-padding: var(--banner-button-padding-inline);
129
+ --button-line-height: var(--banner-button-line-height);
130
+ --button-icon-size: var(--banner-button-icon-size);
131
+ --button-icon-padding: var(--banner-button-icon-padding);
132
+ --button-icon-left-padding: var(--banner-button-icon-left-padding);
133
+ --button-icon-right-padding: var(--banner-button-icon-right-padding);
134
+ margin: var(--banner-button-margin);
135
+ }
136
+ }
137
+
138
+ p > * {
139
+ vertical-align: bottom;
140
+ }
141
+ `;
142
+
143
+ const BannerWrapper = styled.div<{ $color?: BannerColor; $isVisible?: boolean }>`
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ padding: var(--banner-padding);
148
+ color: var(--banner-text-color);
149
+ min-height: var(--banner-min-height);
150
+ position: absolute;
151
+ top: 0;
152
+ left: 0;
153
+ right: 0;
154
+ width: 100%;
155
+ z-index: var(--z-index-overlay);
156
+ transform: ${({ $isVisible }) => ($isVisible ? 'translateY(0)' : 'translateY(-100%)')};
157
+ transition: transform 0.4s ease-out;
158
+ ${({ $color }) =>
159
+ $color &&
160
+ css`
161
+ background-color: var(--banner-${$color}-bg-color);
162
+
163
+ ${BannerContent} {
164
+ p {
165
+ color: var(--banner-${$color}-text-color);
166
+ }
167
+
168
+ a:not([role='button']) {
169
+ color: var(--banner-${$color}-link-color);
170
+ }
171
+ }
172
+ `}
173
+ `;
174
+
175
+ const DismissButton = styled(Button)`
176
+ width: var(--banner-button-size);
177
+ height: var(--banner-button-size);
178
+ padding: var(--banner-button-padding);
179
+ `;
@@ -0,0 +1,10 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const bannerDarkMode = css`
4
+ /**
5
+ * @tokens Banner
6
+ */
7
+
8
+ --banner-warning-text-color: var(--color-white); // @presenter Color
9
+ `;
10
+
@@ -0,0 +1,43 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const banner = css`
4
+ /**
5
+ * @tokens Banner
6
+ */
7
+ --banner-button-size: 22px;
8
+ --banner-button-padding: 3px; // @presenter Spacing
9
+ --banner-button-font-size: var(--font-size-base);
10
+ --banner-button-border-radius: var(--border-radius);
11
+ --banner-button-padding-inline: 1px var(--spacing-sm);
12
+ --banner-button-line-height: var(--line-height-base); // @presenter LineHeight
13
+ --banner-button-icon-size: 14px;
14
+ --banner-button-icon-padding: 5px; // @presenter Spacing
15
+ --banner-button-icon-left-padding: 1px var(--spacing-sm) 1px 10px; // @presenter Spacing
16
+ --banner-button-icon-right-padding: 1px 10px 1px var(--spacing-sm); // @presenter Spacing
17
+ --banner-button-margin: 0 var(--spacing-xs); // @presenter Spacing
18
+ --banner-padding: var(--spacing-xs); // @presenter Spacing
19
+ --banner-link-decoration: var(--link-decoration-hover);
20
+ --banner-min-height: 38px;
21
+ --banner-gap: var(--spacing-xxs); // @presenter Spacing
22
+
23
+ --banner-info-bg-color: var(--color-info-base); // @presenter Color
24
+ --banner-info-text-color: var(--color-static-white); // @presenter Color
25
+ --banner-info-icon-color: var(--color-static-white); // @presenter Color
26
+ --banner-info-link-color: var(--banner-info-text-color); // @presenter Color
27
+
28
+ --banner-success-bg-color: var(--color-success-base); // @presenter Color
29
+ --banner-success-text-color: var(--color-static-white); // @presenter Color
30
+ --banner-success-icon-color: var(--color-static-white); // @presenter Color
31
+ --banner-success-link-color: var(--banner-success-text-color); // @presenter Color
32
+
33
+ --banner-warning-bg-color: var(--color-warning-base); // @presenter Color
34
+ --banner-warning-text-color: var(--color-black); // @presenter Color
35
+ --banner-warning-icon-color: var(--color-black); // @presenter Color
36
+ --banner-warning-link-color: var(--banner-warning-text-color); // @presenter Color
37
+
38
+ --banner-error-bg-color: var(--color-error-base); // @presenter Color
39
+ --banner-error-text-color: var(--color-static-white); // @presenter Color
40
+ --banner-error-icon-color: var(--color-static-white); // @presenter Color
41
+ --banner-error-link-color: var(--banner-error-text-color); // @presenter Color
42
+ `;
43
+
@@ -8,6 +8,7 @@ import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
8
8
  import { ChatIcon } from '@redocly/theme/icons/ChatIcon/ChatIcon';
9
9
  import { AiStarsGradientIcon } from '@redocly/theme/icons/AiStarsGradientIcon/AiStarsGradientIcon';
10
10
  import { RedoclyIcon } from '@redocly/theme/icons/RedoclyIcon/RedoclyIcon';
11
+ import { SearchSessionProvider } from '@redocly/theme/core/contexts';
11
12
 
12
13
  type AIAssistantButtonIconType = 'chat' | 'sparkles' | 'redocly';
13
14
  type AIAssistantButtonType = 'button' | 'icon';
@@ -86,7 +87,7 @@ export function AIAssistantButton() {
86
87
  };
87
88
 
88
89
  return (
89
- <>
90
+ <SearchSessionProvider>
90
91
  <StyledAIAssistantButton
91
92
  variant="outlined"
92
93
  size="medium"
@@ -100,7 +101,7 @@ export function AIAssistantButton() {
100
101
  </StyledAIAssistantButton>
101
102
 
102
103
  {isOpen && <SearchDialog onClose={handleClose} initialMode="ai-dialog" />}
103
- </>
104
+ </SearchSessionProvider>
104
105
  );
105
106
  }
106
107
 
@@ -64,6 +64,7 @@ const LanguageDropdown = styled(Dropdown).attrs(() => ({
64
64
  },
65
65
  }))`
66
66
  display: none;
67
+ height: auto;
67
68
  @media screen and (min-width: ${breakpoints.medium}) {
68
69
  display: block;
69
70
  }
@@ -17,6 +17,7 @@ import { ProductPicker } from '@redocly/theme/components/Product/ProductPicker';
17
17
  import { Button } from '@redocly/theme/components/Button/Button';
18
18
  import { MenuIcon } from '@redocly/theme/icons/MenuIcon/MenuIcon';
19
19
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
20
+ import { Banner } from '@redocly/theme/components/Banner/Banner';
20
21
 
21
22
  export type NavbarProps = {
22
23
  className?: string;
@@ -43,6 +44,7 @@ export function Navbar({ className }: NavbarProps): JSX.Element | null {
43
44
 
44
45
  return (
45
46
  <NavbarWrapper data-component-name="Navbar/Navbar" className={className}>
47
+ <Banner />
46
48
  {isOpen && <MenuMobile hideUserProfile={!!hideUserMenu} />}
47
49
  <NavbarRow>
48
50
  {logo && <NavbarLogo config={logo} />}
@@ -76,16 +78,15 @@ export function Navbar({ className }: NavbarProps): JSX.Element | null {
76
78
 
77
79
  const NavbarWrapper = styled.nav`
78
80
  --text-color: var(--navbar-text-color);
81
+ height: calc(var(--navbar-height) + var(--banner-height));
82
+ transition: height 0.4s ease-out;
79
83
 
80
84
  position: sticky;
81
- display: flex;
82
85
  top: 0;
83
- height: var(--navbar-height);
86
+ display: flex;
87
+ flex-direction: column;
84
88
  flex-shrink: 0;
85
- align-items: center;
86
89
  box-sizing: border-box;
87
- padding: var(--navbar-padding);
88
- border: var(--navbar-border);
89
90
  font-size: var(--navbar-font-size);
90
91
  font-family: var(--navbar-font-family);
91
92
  z-index: var(--z-index-raised);
@@ -110,7 +111,14 @@ const NavbarRow = styled.div`
110
111
  justify-content: space-between;
111
112
  width: 100%;
112
113
  gap: 8px;
114
+ height: var(--navbar-height);
113
115
  max-width: var(--navbar-container-max-width);
116
+ padding: var(--navbar-padding);
117
+ border: var(--navbar-border);
118
+ background: var(--navbar-bg-color);
119
+ box-sizing: border-box;
120
+ margin-top: var(--banner-height);
121
+ transition: margin-top 0.5s ease-out;
114
122
 
115
123
  @media screen and (min-width: ${breakpoints.max}) {
116
124
  max-width: var(--container-max-width);
@@ -6,12 +6,13 @@ import type { JSX } from 'react';
6
6
  import { SearchTrigger } from '@redocly/theme/components/Search/SearchTrigger';
7
7
  import { SearchDialog } from '@redocly/theme/components/Search/SearchDialog';
8
8
  import { useSearchDialog } from '@redocly/theme/core/hooks';
9
+ import { SearchSessionProvider } from '@redocly/theme/core/contexts';
9
10
 
10
11
  export type SearchProps = {
11
12
  className?: string;
12
13
  };
13
14
 
14
- export function Search({ className }: SearchProps): JSX.Element {
15
+ function SearchContent({ className }: SearchProps): JSX.Element {
15
16
  const { isOpen, onOpen, onClose } = useSearchDialog();
16
17
 
17
18
  return (
@@ -22,6 +23,14 @@ export function Search({ className }: SearchProps): JSX.Element {
22
23
  );
23
24
  }
24
25
 
26
+ export function Search({ className }: SearchProps): JSX.Element {
27
+ return (
28
+ <SearchSessionProvider>
29
+ <SearchContent className={className} />
30
+ </SearchSessionProvider>
31
+ );
32
+ }
33
+
25
34
  const SearchWrapper = styled.div`
26
35
  margin-left: auto;
27
36
  `;
@@ -24,6 +24,7 @@ import { SearchGroups } from '@redocly/theme/components/Search/SearchGroups';
24
24
  import { Typography } from '@redocly/theme/components/Typography/Typography';
25
25
  import { SpinnerLoader } from '@redocly/theme/components/Loaders/SpinnerLoader';
26
26
  import { SearchAiDialog } from '@redocly/theme/components/Search/SearchAiDialog';
27
+ import { useSearchSession } from '@redocly/theme/core/contexts';
27
28
  import { SettingsIcon } from '@redocly/theme/icons/SettingsIcon/SettingsIcon';
28
29
  import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
29
30
  import { ReturnKeyIcon } from '@redocly/theme/icons/ReturnKeyIcon/ReturnKeyIcon';
@@ -45,10 +46,10 @@ export function SearchDialog({
45
46
  const { useTranslate, useCurrentProduct, useSearch, useProducts, useAiSearch, useTelemetry } =
46
47
  useThemeHooks();
47
48
  const telemetry = useTelemetry();
49
+ const { searchSessionId, refreshSearchSessionId } = useSearchSession();
48
50
  const products = useProducts();
49
51
  const currentProduct = useCurrentProduct();
50
52
  const [product, setProduct] = useState(currentProduct);
51
- const searchSessionId = `search-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
52
53
  const [mode, setMode] = useState<'search' | 'ai-dialog'>(initialMode);
53
54
  const autoSearchDisabled = mode !== 'search';
54
55
  const {
@@ -63,7 +64,7 @@ export function SearchDialog({
63
64
  advancedSearch,
64
65
  askAi,
65
66
  groupField,
66
- } = useSearch(product?.name, autoSearchDisabled, searchSessionId);
67
+ } = useSearch(product?.name, autoSearchDisabled);
67
68
  const {
68
69
  isFilterOpen,
69
70
  onFilterToggle,
@@ -73,7 +74,7 @@ export function SearchDialog({
73
74
  onQuickFilterReset,
74
75
  } = useSearchFilter(filter, setFilter);
75
76
  const { addSearchHistoryItem } = useRecentSearches();
76
- const aiSearch = useAiSearch({ filter }, searchSessionId);
77
+ const aiSearch = useAiSearch({ filter });
77
78
 
78
79
  const searchInputRef = useRef<HTMLInputElement>(null);
79
80
  const modalRef = useRef<HTMLDivElement>(null);
@@ -103,8 +104,11 @@ export function SearchDialog({
103
104
  addSearchHistoryItem(value);
104
105
  }
105
106
 
107
+ // Refresh the search session id so a new session starts on next open
108
+ refreshSearchSessionId();
109
+
106
110
  onClose();
107
- }, [addSearchHistoryItem, onClose]);
111
+ }, [addSearchHistoryItem, onClose, refreshSearchSessionId]);
108
112
 
109
113
  useDialogHotKeys(modalRef, handleClose);
110
114
 
@@ -276,7 +280,10 @@ export function SearchDialog({
276
280
  <Button
277
281
  variant="secondary"
278
282
  disabled={!aiSearch.conversation.length}
279
- onClick={() => aiSearch.clearConversation()}
283
+ onClick={() => {
284
+ refreshSearchSessionId();
285
+ aiSearch.clearConversation();
286
+ }}
280
287
  tabIndex={0}
281
288
  icon={<EditIcon />}
282
289
  >
@@ -0,0 +1,31 @@
1
+ import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
2
+
3
+ export type SearchSessionContextValue = {
4
+ searchSessionId: string;
5
+ refreshSearchSessionId: () => void;
6
+ };
7
+
8
+ export const SearchSessionContext = createContext<SearchSessionContextValue | null>(null);
9
+
10
+ export const SearchSessionProvider = ({ children }: { children: React.ReactNode }) => {
11
+ const [searchSessionId, setSearchSessionId] = useState(() => crypto.randomUUID());
12
+
13
+ const refreshSearchSessionId = useCallback(() => {
14
+ setSearchSessionId(crypto.randomUUID());
15
+ }, []);
16
+
17
+ const value = useMemo(
18
+ () => ({ searchSessionId, refreshSearchSessionId }),
19
+ [searchSessionId, refreshSearchSessionId],
20
+ );
21
+
22
+ return <SearchSessionContext.Provider value={value}>{children}</SearchSessionContext.Provider>;
23
+ };
24
+
25
+ export function useSearchSession(): SearchSessionContextValue {
26
+ const contextValue = useContext(SearchSessionContext);
27
+ if (!contextValue) {
28
+ throw new Error('useSearchSession must be used within a SearchSessionProvider');
29
+ }
30
+ return contextValue;
31
+ }
@@ -2,3 +2,4 @@ export * from './ThemeDataContext';
2
2
  export * from './CodeWalkthrough/CodeWalkthroughControlsContext';
3
3
  export * from './CodeWalkthrough/CodeWalkthroughStepsContext';
4
4
  export * from './CodeSnippetContext';
5
+ export * from './SearchContext';
@@ -17,6 +17,10 @@ export const useThemeHooks = vi.fn(() => ({
17
17
  sendCodeSnippetReportedMessage: vi.fn(),
18
18
  })),
19
19
  useBreadcrumbs: vi.fn().mockReturnValue({ breadcrumbs: [], siblings: undefined }),
20
+ useBanner: vi.fn(() => ({
21
+ banner: undefined,
22
+ dismissBanner: vi.fn(),
23
+ })),
20
24
  usePageSharedData: vi.fn().mockReturnValue({}),
21
25
  useCatalogClassic: vi.fn(() => ({
22
26
  groups: [],