@redocly/theme 0.59.0-rc.1 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +7 -1
  2. package/lib/components/Buttons/AIAssistantButton.js +6 -2
  3. package/lib/components/Buttons/ConnectMCPButton.d.ts +8 -0
  4. package/lib/components/Buttons/ConnectMCPButton.js +145 -0
  5. package/lib/components/Buttons/variables.d.ts +1 -0
  6. package/lib/components/Buttons/variables.js +42 -2
  7. package/lib/components/Catalog/CatalogEntity/CatalogEntityInfoBar.js +1 -0
  8. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  9. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  10. package/lib/components/Catalog/CatalogEntityIcon.js +2 -1
  11. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +4 -0
  12. package/lib/components/Catalog/CatalogTagsWithTooltip.js +1 -1
  13. package/lib/components/Catalog/variables.js +1 -1
  14. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  15. package/lib/components/Dropdown/Dropdown.js +5 -5
  16. package/lib/components/Menu/MenuItem.js +1 -1
  17. package/lib/components/Navbar/NavbarItem.js +3 -3
  18. package/lib/components/PageActions/PageActions.js +4 -1
  19. package/lib/components/PageActions/variables.js +2 -0
  20. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +1 -2
  21. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  22. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  23. package/lib/components/Search/SearchAiConversationInput.d.ts +3 -1
  24. package/lib/components/Search/SearchAiConversationInput.js +39 -7
  25. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  26. package/lib/components/Search/SearchAiDialog.js +20 -9
  27. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  28. package/lib/components/Search/SearchAiMessage.js +146 -22
  29. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  30. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  31. package/lib/components/Search/SearchDialog.js +36 -5
  32. package/lib/components/Search/SearchGroups.js +2 -2
  33. package/lib/components/Search/variables.js +36 -64
  34. package/lib/components/Segmented/Segmented.d.ts +1 -8
  35. package/lib/components/Segmented/Segmented.js +3 -1
  36. package/lib/components/Select/SelectInput.js +1 -1
  37. package/lib/components/Select/variables.js +2 -2
  38. package/lib/components/Tag/Tag.d.ts +2 -1
  39. package/lib/components/Tag/Tag.js +66 -17
  40. package/lib/components/Tag/variables.dark.js +135 -36
  41. package/lib/components/Tag/variables.js +78 -61
  42. package/lib/core/constants/index.d.ts +1 -0
  43. package/lib/core/constants/index.js +1 -0
  44. package/lib/core/constants/mcp.d.ts +1 -0
  45. package/lib/core/constants/mcp.js +5 -0
  46. package/lib/core/constants/search.d.ts +5 -4
  47. package/lib/core/constants/search.js +4 -5
  48. package/lib/core/hooks/index.d.ts +3 -0
  49. package/lib/core/hooks/index.js +3 -0
  50. package/lib/core/hooks/menu/use-nested-menu.js +1 -1
  51. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  52. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  53. package/lib/core/hooks/use-connect-mcp-button.d.ts +13 -0
  54. package/lib/core/hooks/use-connect-mcp-button.js +50 -0
  55. package/lib/core/hooks/use-mcp-config.d.ts +9 -0
  56. package/lib/core/hooks/use-mcp-config.js +27 -0
  57. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  58. package/lib/core/hooks/use-page-actions.js +98 -95
  59. package/lib/core/hooks/use-product-picker.js +2 -1
  60. package/lib/core/hooks/use-tabs.d.ts +3 -2
  61. package/lib/core/hooks/use-tabs.js +115 -57
  62. package/lib/core/hooks/use-telemetry-fallback.d.ts +10 -8
  63. package/lib/core/hooks/use-telemetry-fallback.js +10 -8
  64. package/lib/core/openapi/index.d.ts +1 -0
  65. package/lib/core/styles/dark.js +4 -0
  66. package/lib/core/styles/global.js +5 -0
  67. package/lib/core/types/hooks.d.ts +2 -2
  68. package/lib/core/types/index.d.ts +1 -0
  69. package/lib/core/types/index.js +1 -0
  70. package/lib/core/types/l10n.d.ts +1 -1
  71. package/lib/core/types/mcp.d.ts +6 -0
  72. package/lib/core/types/mcp.js +3 -0
  73. package/lib/core/types/search.d.ts +11 -4
  74. package/lib/core/types/search.js +6 -0
  75. package/lib/core/types/segmented.d.ts +12 -0
  76. package/lib/core/types/segmented.js +3 -0
  77. package/lib/core/utils/frontmatter-translate.d.ts +6 -0
  78. package/lib/core/utils/frontmatter-translate.js +14 -0
  79. package/lib/core/utils/index.d.ts +2 -0
  80. package/lib/core/utils/index.js +2 -0
  81. package/lib/core/utils/mcp.d.ts +2 -0
  82. package/lib/core/utils/mcp.js +31 -0
  83. package/lib/icons/AiStarsGradientIcon/AiStarsGradientIcon.js +44 -4
  84. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  85. package/lib/icons/ConnectIcon/ConnectIcon.d.ts +9 -0
  86. package/lib/icons/ConnectIcon/ConnectIcon.js +17 -0
  87. package/lib/icons/CubeIcon/CubeIcon.d.ts +9 -0
  88. package/lib/icons/CubeIcon/CubeIcon.js +17 -0
  89. package/lib/icons/HashtagIcon/HashtagIcon.d.ts +9 -0
  90. package/lib/icons/HashtagIcon/HashtagIcon.js +22 -0
  91. package/lib/icons/RedoclyIcon/RedoclyIcon.js +4 -7
  92. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
  93. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
  94. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
  95. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
  96. package/lib/icons/VSCodeIcon/VSCodeIcon.d.ts +9 -0
  97. package/lib/icons/VSCodeIcon/VSCodeIcon.js +17 -0
  98. package/lib/index.d.ts +1 -2
  99. package/lib/index.js +1 -2
  100. package/lib/markdoc/components/Cards/Card.js +1 -28
  101. package/lib/markdoc/components/ConnectMCP/ConnectMCP.d.ts +8 -0
  102. package/lib/markdoc/components/ConnectMCP/ConnectMCP.js +19 -0
  103. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  104. package/lib/markdoc/components/Tabs/TabList.js +197 -47
  105. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  106. package/lib/markdoc/components/Tabs/Tabs.js +57 -12
  107. package/lib/markdoc/components/default.d.ts +1 -0
  108. package/lib/markdoc/components/default.js +1 -0
  109. package/lib/markdoc/default.d.ts +6 -0
  110. package/lib/markdoc/default.js +2 -0
  111. package/lib/markdoc/tags/card.js +0 -1
  112. package/lib/markdoc/tags/connect-mcp.d.ts +2 -0
  113. package/lib/markdoc/tags/connect-mcp.js +27 -0
  114. package/package.json +6 -6
  115. package/src/components/Buttons/AIAssistantButton.tsx +6 -2
  116. package/src/components/Buttons/ConnectMCPButton.tsx +180 -0
  117. package/src/components/Buttons/variables.ts +42 -1
  118. package/src/components/Catalog/CatalogEntity/CatalogEntityInfoBar.tsx +1 -0
  119. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  120. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  121. package/src/components/Catalog/CatalogEntityIcon.tsx +2 -1
  122. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +5 -0
  123. package/src/components/Catalog/CatalogTagsWithTooltip.tsx +1 -5
  124. package/src/components/Catalog/variables.ts +1 -1
  125. package/src/components/Dropdown/Dropdown.tsx +84 -79
  126. package/src/components/Menu/MenuItem.tsx +1 -0
  127. package/src/components/Navbar/NavbarItem.tsx +6 -5
  128. package/src/components/PageActions/PageActions.tsx +5 -1
  129. package/src/components/PageActions/variables.ts +2 -0
  130. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +3 -3
  131. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  132. package/src/components/Search/SearchAiConversationInput.tsx +61 -18
  133. package/src/components/Search/SearchAiDialog.tsx +52 -23
  134. package/src/components/Search/SearchAiMessage.tsx +172 -43
  135. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  136. package/src/components/Search/SearchDialog.tsx +49 -13
  137. package/src/components/Search/SearchGroups.tsx +2 -0
  138. package/src/components/Search/variables.ts +36 -64
  139. package/src/components/Segmented/Segmented.tsx +15 -20
  140. package/src/components/Select/SelectInput.tsx +1 -0
  141. package/src/components/Select/variables.ts +2 -2
  142. package/src/components/Tag/Tag.tsx +35 -19
  143. package/src/components/Tag/variables.dark.ts +135 -36
  144. package/src/components/Tag/variables.ts +78 -61
  145. package/src/core/constants/index.ts +1 -0
  146. package/src/core/constants/mcp.ts +1 -0
  147. package/src/core/constants/search.ts +8 -4
  148. package/src/core/hooks/index.ts +3 -0
  149. package/src/core/hooks/menu/use-nested-menu.ts +2 -2
  150. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  151. package/src/core/hooks/use-connect-mcp-button.ts +79 -0
  152. package/src/core/hooks/use-mcp-config.ts +43 -0
  153. package/src/core/hooks/use-page-actions.ts +148 -126
  154. package/src/core/hooks/use-product-picker.ts +2 -1
  155. package/src/core/hooks/use-tabs.ts +168 -86
  156. package/src/core/hooks/use-telemetry-fallback.ts +10 -8
  157. package/src/core/openapi/index.ts +1 -0
  158. package/src/core/styles/dark.ts +4 -0
  159. package/src/core/styles/global.ts +6 -1
  160. package/src/core/types/hooks.ts +5 -1
  161. package/src/core/types/index.ts +1 -0
  162. package/src/core/types/l10n.ts +13 -0
  163. package/src/core/types/mcp.ts +8 -0
  164. package/src/core/types/search.ts +13 -4
  165. package/src/core/types/segmented.ts +14 -0
  166. package/src/core/utils/frontmatter-translate.ts +9 -0
  167. package/src/core/utils/index.ts +2 -0
  168. package/src/core/utils/mcp.ts +34 -0
  169. package/src/icons/AiStarsGradientIcon/AiStarsGradientIcon.tsx +13 -4
  170. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  171. package/src/icons/ConnectIcon/ConnectIcon.tsx +27 -0
  172. package/src/icons/CubeIcon/CubeIcon.tsx +27 -0
  173. package/src/icons/HashtagIcon/HashtagIcon.tsx +23 -0
  174. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +4 -22
  175. package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
  176. package/src/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.tsx +35 -0
  177. package/src/icons/VSCodeIcon/VSCodeIcon.tsx +29 -0
  178. package/src/index.ts +1 -2
  179. package/src/markdoc/components/Cards/Card.tsx +1 -28
  180. package/src/markdoc/components/ConnectMCP/ConnectMCP.tsx +28 -0
  181. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  182. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
  183. package/src/markdoc/components/default.ts +1 -0
  184. package/src/markdoc/default.ts +2 -0
  185. package/src/markdoc/tags/card.ts +0 -1
  186. package/src/markdoc/tags/connect-mcp.ts +25 -0
  187. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.d.ts +0 -1
  188. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.js +0 -11
  189. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.d.ts +0 -1
  190. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.js +0 -5
  191. package/lib/ext/process-scorecard.d.ts +0 -5
  192. package/lib/ext/process-scorecard.js +0 -11
  193. package/src/components/OpenApiDocs/hooks/AdditionalOverviewInfo.tsx +0 -9
  194. package/src/components/OpenApiDocs/hooks/AfterOpenApiDescription.tsx +0 -1
  195. package/src/ext/process-scorecard.ts +0 -13
@@ -13,39 +13,75 @@ type Tabs = {
13
13
  overflow: number[];
14
14
  };
15
15
 
16
+ type UseTabsReturn = {
17
+ setTabRef: (element: HTMLButtonElement | null, index: number) => void;
18
+ onTabClick: (labelOrIndex: string | number) => void;
19
+ handleKeyboard: (event: React.KeyboardEvent, index: number) => void;
20
+ visibleTabs: number[];
21
+ overflowTabs: number[];
22
+ isReady: boolean;
23
+ };
24
+
25
+ type UseActiveTabProps = {
26
+ initialTab: string;
27
+ tabsId?: string;
28
+ };
29
+
16
30
  const MORE_BUTTON_WIDTH = 80;
17
31
  const TABS_GAP = 8;
18
32
 
19
- export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: UseTabsProps) {
33
+ export function useTabs({
34
+ activeTab,
35
+ onTabChange,
36
+ totalTabs,
37
+ containerRef,
38
+ }: UseTabsProps): UseTabsReturn {
20
39
  const [tabs, setTabs] = useState<Tabs>({
21
40
  visible: Array.from({ length: totalTabs }, (_, i) => i),
22
41
  overflow: [],
23
42
  });
24
-
43
+ const [isReady, setIsReady] = useState(false);
44
+ const isFirstCalculation = useRef(true);
25
45
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
26
46
  const tabWidthsRef = useRef<number[]>([]);
27
47
  const tabLabelsRef = useRef<string[]>([]);
48
+ const activeTabRef = useRef(activeTab);
49
+ const calculateVisibleTabsRef = useRef<(() => void) | null>(null);
28
50
 
29
- const allTabsHidden = useMemo(() => tabs.visible.length === 0, [tabs.visible]);
51
+ // Synchronously update ref before any callbacks or effects run
52
+ activeTabRef.current = activeTab;
30
53
 
31
- const setTabRef = useCallback((element: HTMLButtonElement | null, index: number) => {
32
- tabRefs.current[index] = element;
54
+ const setTabRef = useCallback(
55
+ (element: HTMLButtonElement | null, index: number) => {
56
+ tabRefs.current[index] = element;
33
57
 
34
- const width = element?.offsetWidth;
35
- if (width) {
36
- tabWidthsRef.current[index] = width;
37
- }
58
+ const width = element?.offsetWidth;
59
+ if (width) {
60
+ tabWidthsRef.current[index] = width;
61
+ }
38
62
 
39
- const label = element?.getAttribute('data-label');
40
- if (label) {
41
- tabLabelsRef.current[index] = label;
42
- }
43
- }, []);
63
+ const label = element?.getAttribute('data-label');
64
+ if (label) {
65
+ tabLabelsRef.current[index] = label;
66
+ }
44
67
 
45
- const focusTab = (index: number) => {
68
+ // Trigger calculation once all tabs are registered
69
+ if (
70
+ isFirstCalculation.current &&
71
+ tabWidthsRef.current.length >= totalTabs &&
72
+ tabLabelsRef.current.length >= totalTabs &&
73
+ calculateVisibleTabsRef.current
74
+ ) {
75
+ requestAnimationFrame(calculateVisibleTabsRef.current);
76
+ }
77
+ },
78
+ [totalTabs],
79
+ );
80
+
81
+ const focusTab = useCallback((index: number) => {
46
82
  const currentElement = tabRefs.current[index];
47
83
  currentElement?.focus();
48
- };
84
+ }, []);
49
85
 
50
86
  const onTabSelect = useCallback(
51
87
  (index: number) => {
@@ -53,7 +89,7 @@ export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: Use
53
89
  const label = tabRefs.current[index]?.getAttribute('data-label');
54
90
  if (label) onTabChange(label);
55
91
  },
56
- [onTabChange],
92
+ [onTabChange, focusTab],
57
93
  );
58
94
 
59
95
  const handleKeyboard = useCallback(
@@ -77,30 +113,19 @@ export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: Use
77
113
  [totalTabs, onTabSelect],
78
114
  );
79
115
 
80
- const replaceLastVisibleTabWithClickedOverflowTab = useCallback(
81
- (clickedIndex: number) => {
82
- const { visible: visibleTabs, overflow: overflowTabs } = tabs;
83
-
84
- // Indexes of visible tabs should be sorted(asc), to replace the last visible tab with the clicked tab
85
- const newVisibleTabs = [...visibleTabs].sort((a, b) => a - b);
86
- const newOverflowTabs = [...overflowTabs];
87
-
88
- const clickedIdxInOverflow = newOverflowTabs.indexOf(clickedIndex);
89
- if (clickedIdxInOverflow !== -1) {
90
- const lastVisible = newVisibleTabs[newVisibleTabs.length - 1];
91
- newOverflowTabs.splice(clickedIdxInOverflow, 1);
92
- newOverflowTabs.unshift(lastVisible);
93
- newVisibleTabs.splice(newVisibleTabs.length - 1, 1);
94
- newVisibleTabs.unshift(clickedIndex);
95
- }
116
+ const replaceLastVisibleTabWithClickedOverflowTab = useCallback((clickedIndex: number) => {
117
+ setTabs((prevTabs) => {
118
+ const { visible: visibleTabs, overflow: overflowTabs } = prevTabs;
96
119
 
97
- setTabs({
98
- visible: newVisibleTabs,
99
- overflow: newOverflowTabs,
100
- });
101
- },
102
- [tabs],
103
- );
120
+ const sortedVisible = [...visibleTabs].sort((a, b) => a - b);
121
+ const lastVisible = sortedVisible[sortedVisible.length - 1];
122
+
123
+ return {
124
+ visible: visibleTabs.map((idx) => (idx === lastVisible ? clickedIndex : idx)),
125
+ overflow: overflowTabs.map((idx) => (idx === clickedIndex ? lastVisible : idx)),
126
+ };
127
+ });
128
+ }, []);
104
129
 
105
130
  const onTabClick = useCallback(
106
131
  (labelOrIndex: string | number) => {
@@ -111,18 +136,18 @@ export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: Use
111
136
 
112
137
  if (clickedIndex === -1) return;
113
138
 
114
- const hasOverflowTabs = tabs.overflow.length > 0;
115
- if (hasOverflowTabs && !allTabsHidden && tabs.overflow.includes(clickedIndex)) {
139
+ const label = tabLabelsRef.current[clickedIndex];
140
+ if (!label) return;
141
+
142
+ // If this is an overflow tab, replace it with a visible one
143
+ if (tabs.overflow.includes(clickedIndex)) {
116
144
  replaceLastVisibleTabWithClickedOverflowTab(clickedIndex);
117
145
  }
118
146
 
119
- const label = tabLabelsRef.current[clickedIndex];
120
- if (label) {
121
- onTabChange(label);
122
- focusTab(clickedIndex);
123
- }
147
+ onTabChange(label);
148
+ focusTab(clickedIndex);
124
149
  },
125
- [allTabsHidden, tabs.overflow, onTabChange, replaceLastVisibleTabWithClickedOverflowTab],
150
+ [tabs.overflow, onTabChange, replaceLastVisibleTabWithClickedOverflowTab, focusTab],
126
151
  );
127
152
 
128
153
  const calculateVisibleTabs = useCallback(() => {
@@ -131,87 +156,141 @@ export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: Use
131
156
 
132
157
  const containerWidth = container.offsetWidth;
133
158
  const tabWidths = tabWidthsRef.current;
134
- const activeTabIndex = tabRefs.current.findIndex(
135
- (ref) => ref?.getAttribute('data-label') === activeTab,
136
- );
159
+ const tabLabels = tabLabelsRef.current;
160
+
161
+ // Wait until all tabs are registered before calculating
162
+ if (tabWidths.length < totalTabs || tabLabels.length < totalTabs) {
163
+ return;
164
+ }
165
+
166
+ // Check if container has proper width (not zero)
167
+ if (containerWidth === 0) {
168
+ return;
169
+ }
170
+
171
+ // Find active tab index by label in tabLabelsRef, not by DOM element
172
+ // because tab might not be rendered if it's in overflow
173
+ const activeTabIndex = tabLabels.findIndex((label) => label === activeTabRef.current);
137
174
 
138
- // Active tab should always be visible, so we include it at the beginning of the array
139
175
  let tabsWidth = activeTabIndex !== -1 ? tabWidths[activeTabIndex] : 0;
140
- const visible = activeTabIndex !== -1 ? [activeTabIndex] : [];
141
- const overflow = [];
176
+ const visibleTabs = activeTabIndex !== -1 ? [activeTabIndex] : [];
177
+ const overflowTabs = [];
142
178
 
143
179
  for (let i = 0; i < tabWidths.length; i++) {
144
- // Skip active tab, it was added initially
145
- if (i === activeTabIndex) {
146
- continue;
147
- }
180
+ if (i === activeTabIndex) continue;
148
181
 
149
182
  const tabWidthWithGap = tabWidths[i] + TABS_GAP;
150
183
  const projectedWidth = tabsWidth + tabWidthWithGap;
151
184
 
152
185
  if (projectedWidth <= containerWidth) {
153
- visible.push(i);
186
+ visibleTabs.push(i);
154
187
  tabsWidth += tabWidthWithGap;
155
188
  } else {
156
- overflow.push(i);
189
+ overflowTabs.push(i);
157
190
  }
158
191
  }
159
192
 
160
- if (overflow.length > 0) {
193
+ if (overflowTabs.length > 0) {
161
194
  tabsWidth += MORE_BUTTON_WIDTH;
162
195
 
163
- // Remove tabs starting from the end of the array until the width of the visible tabs is less than the container width
164
- while (tabsWidth > containerWidth && visible.length) {
165
- const removed = visible.pop();
166
- if (removed !== undefined) {
167
- overflow.unshift(removed);
196
+ while (tabsWidth > containerWidth && visibleTabs.length > 1) {
197
+ const removed = visibleTabs.pop();
198
+ // Never remove the active tab - it should always stay visible or be the last one
199
+ if (removed !== undefined && removed !== activeTabIndex) {
200
+ overflowTabs.unshift(removed);
168
201
  tabsWidth -= tabWidths[removed];
202
+ } else if (removed === activeTabIndex) {
203
+ // Put it back if we accidentally removed the active tab
204
+ visibleTabs.push(removed);
205
+ break;
169
206
  }
170
207
  }
208
+
209
+ // If even with only the active tab visible, it doesn't fit with More button,
210
+ // move all tabs to overflow (show only dropdown)
211
+ if (tabsWidth > containerWidth && visibleTabs.length === 1) {
212
+ overflowTabs.unshift(...visibleTabs);
213
+ visibleTabs.length = 0;
214
+ }
171
215
  }
172
216
 
173
217
  setTabs({
174
- visible,
175
- overflow,
218
+ visible: visibleTabs,
219
+ overflow: overflowTabs,
176
220
  });
177
- }, [containerRef, activeTab]);
178
221
 
222
+ // Set ready state on first calculation
223
+ if (isFirstCalculation.current) {
224
+ isFirstCalculation.current = false;
225
+ setIsReady(true);
226
+ }
227
+ }, [containerRef, totalTabs]);
228
+
229
+ // Store calculateVisibleTabs in ref for use in setTabRef
230
+ calculateVisibleTabsRef.current = calculateVisibleTabs;
231
+
232
+ // Reset isFirstCalculation when totalTabs changes (new page/tabs)
233
+ useEffect(() => {
234
+ isFirstCalculation.current = true;
235
+ setIsReady(false);
236
+ // Clear refs so we wait for new tabs to register
237
+ tabWidthsRef.current = [];
238
+ tabLabelsRef.current = [];
239
+ }, [totalTabs]);
240
+
241
+ // Call calculateVisibleTabs on first render and resize
179
242
  useEffect(() => {
180
- if (!containerRef?.current) return;
243
+ const container = containerRef?.current;
244
+ if (!container) return;
245
+
246
+ let resizeTimeout: number | null = null;
247
+
248
+ // Use ResizeObserver to wait until container has proper size
249
+ const resizeObserver = new ResizeObserver(() => {
250
+ if (resizeTimeout) cancelAnimationFrame(resizeTimeout);
251
+ resizeTimeout = requestAnimationFrame(calculateVisibleTabs);
252
+ });
253
+
254
+ resizeObserver.observe(container);
181
255
 
182
- let resizeTimeout = requestAnimationFrame(calculateVisibleTabs);
183
256
  const handleResize = () => {
184
- if (resizeTimeout) {
185
- cancelAnimationFrame(resizeTimeout);
186
- }
257
+ if (resizeTimeout) cancelAnimationFrame(resizeTimeout);
187
258
  resizeTimeout = requestAnimationFrame(calculateVisibleTabs);
188
259
  };
189
260
 
190
261
  window.addEventListener('resize', handleResize);
262
+
191
263
  return () => {
264
+ resizeObserver.disconnect();
192
265
  window.removeEventListener('resize', handleResize);
193
- cancelAnimationFrame(resizeTimeout);
266
+ if (resizeTimeout) cancelAnimationFrame(resizeTimeout);
194
267
  };
195
268
  }, [containerRef, totalTabs, calculateVisibleTabs]);
196
269
 
270
+ // Recalculate when activeTab changes to ensure it's visible
271
+ useEffect(() => {
272
+ if (!containerRef?.current || isFirstCalculation.current) return;
273
+ requestAnimationFrame(calculateVisibleTabs);
274
+ }, [activeTab, containerRef, calculateVisibleTabs]);
275
+
197
276
  return {
198
277
  setTabRef,
199
278
  onTabClick,
200
279
  handleKeyboard,
201
280
  visibleTabs: tabs.visible,
202
281
  overflowTabs: tabs.overflow,
203
- allTabsHidden,
282
+ isReady,
204
283
  };
205
284
  }
206
285
 
207
- type UseActiveTabProps = {
208
- initialTab: string;
209
- tabsId?: string;
210
- };
211
-
212
286
  export const useActiveTab = ({ initialTab, tabsId }: UseActiveTabProps) => {
213
287
  const [searchParams, setSearchParams] = useSearchParams();
214
- const [activeTab, setActiveTab] = useState(getInitialTab({ initialTab, searchParams, tabsId }));
288
+ const initialTabValue = useMemo(
289
+ () => getInitialTab({ initialTab, searchParams, tabsId }),
290
+ // eslint-disable-next-line react-hooks/exhaustive-deps
291
+ [],
292
+ );
293
+ const [activeTab, setActiveTab] = useState(initialTabValue);
215
294
  const prevActiveTabRef = useRef(activeTab);
216
295
 
217
296
  useEffect(() => {
@@ -228,10 +307,13 @@ export const useActiveTab = ({ initialTab, tabsId }: UseActiveTabProps) => {
228
307
  });
229
308
  }, [activeTab, setSearchParams, tabsId]);
230
309
 
231
- return {
232
- activeTab,
233
- setActiveTab,
234
- };
310
+ return useMemo(
311
+ () => ({
312
+ activeTab,
313
+ setActiveTab,
314
+ }),
315
+ [activeTab],
316
+ );
235
317
  };
236
318
 
237
319
  type GetInitialTabProps = {
@@ -37,20 +37,22 @@ export const useTelemetryFallback = () => ({
37
37
  sendLanguagePickerLocaleChangedMessage: () => {},
38
38
  sendSearchOpenedMessage: () => {},
39
39
  sendSearchQueryMessage: () => {},
40
+ sendSearchAiOpenedMessage: () => {},
40
41
  sendSearchAIQueryMessage: () => {},
42
+ sendSearchAIFeedbackMessage: () => {},
41
43
  sendFeedbackMessage: () => {},
42
44
  sendSearchResultClickedMessage: () => {},
43
45
  sendRedirectMessage: () => {},
44
46
  sendOpenapiDocsMessage: () => {},
45
47
  sendCopyCodeSnippetClickedMessage: () => {},
46
- sendOpenapiDocsViewedMessage: () => {},
47
- sendOpenapiDocsPerformanceMetricsMessage: () => {},
48
- sendOpenapiDocsDownloadDefinitionClickedMessage: () => {},
49
- sendOpenapiDocsSelectLanguageClickedMessage: () => {},
50
- sendOpenapiDocsExpandCollapseAllClickedMessage: () => {},
51
- sendOpenapiDocsSwitchServersClickedMessage: () => {},
52
- sendOpenapiDocsExamplesSwitcherClickedMessage: () => {},
53
- sendOpenapiDocsTryItOpenedMessage: () => {},
48
+ sendViewedMessage: () => {},
49
+ sendPerformanceMetricsMessage: () => {},
50
+ sendDownloadDefinitionClickedMessage: () => {},
51
+ sendSelectLanguageClickedMessage: () => {},
52
+ sendExpandCollapseAllClickedMessage: () => {},
53
+ sendSwitchServersClickedMessage: () => {},
54
+ sendExamplesSwitcherClickedMessage: () => {},
55
+ sendTryItOpenedMessage: () => {},
54
56
  sendAsyncapiDocsViewedMessage: () => {},
55
57
  sendAsyncapiDocsPerformanceMetricsMessage: () => {},
56
58
  sendAsyncapiDocsSwitchMessageClickedMessage: () => {},
@@ -8,6 +8,7 @@ export type { UserClaims } from '../types/user-claims';
8
8
  export type { OperationParameter, ParameterHighlight } from '../types/search';
9
9
  export type { TFunction, TOptions } from '../types/l10n';
10
10
  export type { SelectOption, SelectProps } from '../types/select';
11
+ export type { SegmentedOption, SegmentedProps } from '../types/segmented';
11
12
  export { IS_BROWSER } from '../utils/dom';
12
13
  export {
13
14
  addLeadingSlash,
@@ -36,6 +36,10 @@ const replayDarkMode = css`
36
36
  --replay-server-variable-bg-color-hover: rgba(31, 10, 144, 0.4); // @presenter Color
37
37
  --replay-path-parameter-bg-color-hover: rgba(5, 88, 99, 0.4); // @presenter Color
38
38
 
39
+ --replay-runtime-expression-color: rgba(147, 166, 249, 1); // @presenter Color
40
+ --replay-runtime-expression-bg-color: rgba(147, 166, 249, 0.16); // @presenter Color
41
+ --replay-operators-color: rgba(168, 143, 88, 1); // @presenter Color
42
+
39
43
  // @tokens End
40
44
  `;
41
45
 
@@ -19,7 +19,7 @@ import { checkbox } from '@redocly/theme/icons/CheckboxIcon/variables';
19
19
  import { admonition } from '@redocly/theme/components/Admonition/variables';
20
20
  import { footer } from '@redocly/theme/components/Footer/variables';
21
21
  import { button } from '@redocly/theme/components/Button/variables';
22
- import { aiAssistantButton } from '@redocly/theme/components/Buttons/variables';
22
+ import { aiAssistantButton, connectMCPButton } from '@redocly/theme/components/Buttons/variables';
23
23
  import { navbar } from '@redocly/theme/components/Navbar/variables';
24
24
  import { search } from '@redocly/theme/components/Search/variables';
25
25
  import { menu, mobileMenu } from '@redocly/theme/components/Menu/variables';
@@ -1224,6 +1224,10 @@ const replay = css`
1224
1224
  --replay-server-variable-bg-color-hover: rgba(119, 45, 240, 0.16); // @presenter Color
1225
1225
  --replay-path-parameter-bg-color-hover: rgba(4, 117, 161, 0.16); // @presenter Color
1226
1226
 
1227
+ --replay-runtime-expression-color: rgba(54, 90, 249, 1); // @presenter Color
1228
+ --replay-runtime-expression-bg-color: rgba(54, 90, 249, 0.08); // @presenter Color
1229
+ --replay-operators-color: rgba(193, 142, 31, 1); // @presenter Color
1230
+
1227
1231
  // @tokens End
1228
1232
  `;
1229
1233
 
@@ -1239,6 +1243,7 @@ export const styles = css`
1239
1243
  ${breadcrumbs}
1240
1244
  ${button}
1241
1245
  ${aiAssistantButton}
1246
+ ${connectMCPButton}
1242
1247
  ${cards}
1243
1248
  ${catalog}
1244
1249
  ${catalogClassic}
@@ -86,6 +86,7 @@ export type ThemeHooks = {
86
86
  useSearch: (
87
87
  product?: string,
88
88
  autoSearchDisabled?: boolean,
89
+ searchSessionId?: string,
89
90
  ) => {
90
91
  query: string;
91
92
  setQuery: React.Dispatch<React.SetStateAction<string>>;
@@ -107,7 +108,10 @@ export type ThemeHooks = {
107
108
  advancedSearch?: boolean;
108
109
  askAi?: boolean;
109
110
  };
110
- useAiSearch: (options?: { filter?: SearchFilterItem[] }) => {
111
+ useAiSearch: (
112
+ options?: { filter?: SearchFilterItem[] },
113
+ searchSessionId?: string,
114
+ ) => {
111
115
  askQuestion: (question: string, history?: AiSearchConversationItem[]) => void;
112
116
  isGeneratingResponse: boolean;
113
117
  question: string;
@@ -1,6 +1,7 @@
1
1
  export * from './breadcrumb';
2
2
  export * from './catalog';
3
3
  export * from './l10n';
4
+ export * from './mcp';
4
5
  export * from './feedback';
5
6
  export * from './hooks';
6
7
  export * from './markdown';
@@ -86,6 +86,7 @@ export type TranslationKey =
86
86
  | 'search.ai.welcomeText'
87
87
  | 'search.ai.newConversation'
88
88
  | 'search.ai.backToSearch'
89
+ | 'search.ai.back'
89
90
  | 'search.ai.placeholder'
90
91
  | 'search.ai.generatingResponse'
91
92
  | 'search.ai.followUpQuestion'
@@ -94,6 +95,9 @@ export type TranslationKey =
94
95
  | 'search.ai.resourcesFound'
95
96
  | 'search.ai.resourcesFound.basedOn'
96
97
  | 'search.ai.resourcesFound.resources'
98
+ | 'search.ai.feedback.title'
99
+ | 'search.ai.feedback.detailsPlaceholder'
100
+ | 'search.ai.feedback.thanks'
97
101
  | 'search.ai.button'
98
102
  | 'search.ai.label'
99
103
  | 'search.ai.disclaimer'
@@ -211,6 +215,13 @@ export type TranslationKey =
211
215
  | 'page.actions.cursorMcpButtonText'
212
216
  | 'page.actions.cursorMcpTitle'
213
217
  | 'page.actions.cursorMcpDescription'
218
+ | 'page.actions.connectMcp'
219
+ | 'page.actions.connectMcp.cursor'
220
+ | 'page.actions.connectMcp.cursorDescription'
221
+ | 'page.actions.connectMcp.vscode'
222
+ | 'page.actions.connectMcp.vscodeDescription'
223
+ | 'page.actions.connectMcp.copyConfig'
224
+ | 'page.actions.connectMcp.copyConfigDescription'
214
225
  | 'openapi.download.description.title'
215
226
  | 'openapi.info.title'
216
227
  | 'openapi.info.contact.url'
@@ -267,6 +278,8 @@ export type TranslationKey =
267
278
  | 'openapi.noResponseExample'
268
279
  | 'openapi.discriminator.searchPlaceholder'
269
280
  | 'openapi.discriminator.searchNoResults'
281
+ | 'openapi.discriminator.defaultMapping'
282
+ | 'openapi.discriminator.defaultMappingTooltip'
270
283
  | 'openapi.noResponseContent'
271
284
  | 'openapi.noRequestPayload'
272
285
  | 'openapi.hidePattern'
@@ -0,0 +1,8 @@
1
+ export type MCPClientType = 'cursor' | 'vscode';
2
+
3
+ export type MCPOption = 'cursor' | 'vscode' | 'copy';
4
+
5
+ export type McpConnectionParams = {
6
+ serverName: string;
7
+ url: string;
8
+ };
@@ -93,11 +93,20 @@ export type AiSearchErrorConfig = {
93
93
  messageDefault: string;
94
94
  };
95
95
 
96
+ export enum FeedbackType {
97
+ Like = 'like',
98
+ Dislike = 'dislike',
99
+ }
100
+
101
+ export type SearchAiMessageResource = {
102
+ url: string;
103
+ title: string;
104
+ };
105
+
96
106
  export type AiSearchConversationItem = {
97
107
  role: AiSearchConversationRole;
98
108
  content: string;
99
- resources?: {
100
- url: string;
101
- title: string;
102
- }[];
109
+ resources?: SearchAiMessageResource[];
110
+ messageId?: string;
111
+ feedback?: FeedbackType;
103
112
  };
@@ -0,0 +1,14 @@
1
+ import type React from 'react';
2
+ import type { SelectOption } from './select';
3
+
4
+ export type SegmentedOption<T> = SelectOption<T> & {
5
+ divider?: React.ReactNode;
6
+ };
7
+
8
+ export type SegmentedProps<T = any> = {
9
+ options: SegmentedOption<T>[];
10
+ value: T;
11
+ onChange: ({ label, value }: SegmentedOption<T>) => void;
12
+ className?: string;
13
+ size?: 'regular' | 'small';
14
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utility function for use in React frontmatter
3
+ * This function creates a string that will be processed by the frontmatter loader
4
+ * to convert it into a translation key object
5
+ */
6
+ export const frontmatterTranslate = (key: string, defaultValue: string): string => {
7
+ const escapedDefaultValue = defaultValue.replace(/'/g, "\\'");
8
+ return `frontmatterTranslate('${key}', '${escapedDefaultValue}')`;
9
+ };
@@ -1,6 +1,7 @@
1
1
  export * from './clipboard-service';
2
2
  export * from './css-variables';
3
3
  export * from './add-line-numbers';
4
+ export * from './mcp';
4
5
  export * from './media-css';
5
6
  export * from './theme-helpers';
6
7
  export * from './class-names';
@@ -38,3 +39,4 @@ export * from './enhanced-smoothstep';
38
39
  export * from './icon-resolver';
39
40
  export * from './dynamic';
40
41
  export * from './tabs';
42
+ export * from './frontmatter-translate';
@@ -0,0 +1,34 @@
1
+ import type { MCPClientType, McpConnectionParams } from '../types';
2
+
3
+ function generateCursorMCPDeepLink(config: McpConnectionParams): string {
4
+ const cursorConfig = {
5
+ url: config.url,
6
+ description: 'MCP Server',
7
+ };
8
+ const encodedConfig = btoa(JSON.stringify(cursorConfig));
9
+ return `cursor://anysphere.cursor-deeplink/mcp/install?name=${config.serverName}&config=${encodedConfig}`;
10
+ }
11
+
12
+ function generateVSCodeMCPDeepLink(config: McpConnectionParams): string {
13
+ const vscodeConfig = {
14
+ name: config.serverName,
15
+ url: config.url,
16
+ type: 'http',
17
+ };
18
+ const encodedConfig = encodeURIComponent(JSON.stringify(vscodeConfig));
19
+ return `vscode:mcp/install?${encodedConfig}`;
20
+ }
21
+
22
+ export function generateMCPDeepLink(
23
+ clientType: MCPClientType,
24
+ config: McpConnectionParams,
25
+ ): string {
26
+ switch (clientType) {
27
+ case 'cursor':
28
+ return generateCursorMCPDeepLink(config);
29
+ case 'vscode':
30
+ return generateVSCodeMCPDeepLink(config);
31
+ default:
32
+ return generateCursorMCPDeepLink(config);
33
+ }
34
+ }
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { IconProps } from '@redocly/theme/icons/types';
@@ -15,9 +15,18 @@ export interface AiStarsGradientIconProps extends IconProps {
15
15
  const Icon = (props: AiStarsGradientIconProps) => {
16
16
  const { color = '', background, borderRadius, padding, margin, ...restProps } = props;
17
17
 
18
- const resolvedColor = color.startsWith('var(')
19
- ? getComputedStyle(document.documentElement).getPropertyValue(color.slice(4, -1)).trim()
20
- : color;
18
+ const [resolvedColor, setResolvedColor] = useState(color);
19
+
20
+ useEffect(() => {
21
+ const resolvedColor = color.startsWith('var(')
22
+ ? window
23
+ .getComputedStyle(document.documentElement)
24
+ .getPropertyValue(color.slice(4, -1))
25
+ .trim()
26
+ : color;
27
+
28
+ setResolvedColor(resolvedColor);
29
+ }, [color]);
21
30
 
22
31
  const isColorOverridden = resolvedColor && resolvedColor !== 'none';
23
32
  const fill = isColorOverridden ? resolvedColor : 'url(#gradient)';