@redocly/theme 0.58.0-next.9 → 0.59.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/lib/components/Catalog/CatalogEntity/CatalogEntity.d.ts +5 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntity.js +4 -4
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +3 -3
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  5. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  6. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.d.ts +5 -1
  7. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +9 -7
  8. package/lib/components/CodeBlock/CodeBlock.d.ts +5 -12
  9. package/lib/components/CodeBlock/CodeBlockControls.d.ts +3 -3
  10. package/lib/components/CodeBlock/CodeBlockControls.js +1 -1
  11. package/lib/components/CodeBlock/CodeBlockDropdown.d.ts +2 -2
  12. package/lib/components/CodeBlock/CodeBlockDropdown.js +4 -13
  13. package/lib/components/CodeBlock/CodeBlockTabs.d.ts +2 -2
  14. package/lib/components/CodeBlock/CodeBlockTabs.js +4 -3
  15. package/lib/components/JsonViewer/JsonViewer.d.ts +1 -1
  16. package/lib/components/JsonViewer/JsonViewer.js +9 -10
  17. package/lib/components/PageActions/PageActions.d.ts +4 -1
  18. package/lib/components/PageActions/PageActions.js +2 -2
  19. package/lib/components/Panel/variables.js +1 -0
  20. package/lib/components/Tag/Tag.d.ts +3 -2
  21. package/lib/components/Tag/Tag.js +21 -5
  22. package/lib/components/Tag/variables.dark.js +135 -0
  23. package/lib/components/Tag/variables.js +120 -58
  24. package/lib/core/constants/catalog.js +4 -0
  25. package/lib/core/contexts/CodeSnippetContext.d.ts +14 -6
  26. package/lib/core/contexts/CodeSnippetContext.js +57 -14
  27. package/lib/core/hooks/use-codeblock-tabs-controls.d.ts +2 -2
  28. package/lib/core/hooks/use-local-state.js +22 -18
  29. package/lib/core/hooks/use-page-actions.d.ts +2 -1
  30. package/lib/core/hooks/use-page-actions.js +48 -6
  31. package/lib/core/hooks/use-tabs.d.ts +11 -6
  32. package/lib/core/hooks/use-tabs.js +117 -207
  33. package/lib/core/openapi/index.d.ts +1 -0
  34. package/lib/core/openapi/index.js +3 -1
  35. package/lib/core/types/l10n.d.ts +1 -1
  36. package/lib/core/types/open-api-server.d.ts +1 -0
  37. package/lib/core/utils/index.d.ts +1 -0
  38. package/lib/core/utils/index.js +1 -0
  39. package/lib/core/utils/tabs.d.ts +1 -0
  40. package/lib/core/utils/tabs.js +8 -0
  41. package/lib/icons/CursorIcon/CursorIcon.d.ts +9 -0
  42. package/lib/icons/CursorIcon/CursorIcon.js +22 -0
  43. package/lib/layouts/DocumentationLayout.js +1 -3
  44. package/lib/markdoc/components/CodeGroup/CodeGroup.js +49 -27
  45. package/lib/markdoc/components/Tabs/Tab.js +1 -1
  46. package/lib/markdoc/components/Tabs/TabList.d.ts +2 -14
  47. package/lib/markdoc/components/Tabs/TabList.js +65 -16
  48. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -2
  49. package/lib/markdoc/components/Tabs/Tabs.js +11 -87
  50. package/lib/markdoc/tags/tabs.js +5 -0
  51. package/package.json +4 -4
  52. package/src/components/Catalog/CatalogEntity/CatalogEntity.tsx +15 -2
  53. package/src/components/Catalog/CatalogEntity/CatalogEntityMetadata.tsx +3 -3
  54. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  55. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  56. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +27 -18
  57. package/src/components/CodeBlock/CodeBlock.tsx +5 -11
  58. package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
  59. package/src/components/CodeBlock/CodeBlockDropdown.tsx +11 -20
  60. package/src/components/CodeBlock/CodeBlockTabs.tsx +8 -8
  61. package/src/components/JsonViewer/JsonViewer.tsx +16 -9
  62. package/src/components/PageActions/PageActions.tsx +6 -4
  63. package/src/components/Panel/variables.ts +1 -0
  64. package/src/components/Tag/Tag.tsx +33 -8
  65. package/src/components/Tag/variables.dark.ts +135 -0
  66. package/src/components/Tag/variables.ts +120 -58
  67. package/src/core/constants/catalog.ts +4 -0
  68. package/src/core/contexts/CodeSnippetContext.tsx +54 -18
  69. package/src/core/hooks/use-codeblock-tabs-controls.ts +2 -2
  70. package/src/core/hooks/use-local-state.ts +28 -19
  71. package/src/core/hooks/use-page-actions.ts +63 -6
  72. package/src/core/hooks/use-tabs.ts +160 -238
  73. package/src/core/openapi/index.ts +1 -0
  74. package/src/core/types/l10n.ts +13 -0
  75. package/src/core/types/open-api-server.ts +1 -0
  76. package/src/core/utils/index.ts +1 -0
  77. package/src/core/utils/tabs.ts +4 -0
  78. package/src/icons/CursorIcon/CursorIcon.tsx +35 -0
  79. package/src/layouts/DocumentationLayout.tsx +3 -10
  80. package/src/markdoc/components/CodeGroup/CodeGroup.tsx +81 -52
  81. package/src/markdoc/components/Tabs/Tab.tsx +1 -0
  82. package/src/markdoc/components/Tabs/TabList.tsx +85 -30
  83. package/src/markdoc/components/Tabs/Tabs.tsx +12 -125
  84. package/src/markdoc/tags/tabs.ts +5 -0
@@ -1,115 +1,59 @@
1
- import { useCallback, useRef, useState, useEffect } from 'react';
1
+ import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
2
3
 
3
4
  type UseTabsProps = {
4
- initialTab: string;
5
+ activeTab: string;
6
+ onTabChange: (tab: string) => void;
5
7
  totalTabs: number;
6
8
  containerRef?: React.RefObject<HTMLElement | null>;
7
9
  };
8
10
 
9
- export function useTabs({ initialTab, totalTabs, containerRef }: UseTabsProps) {
10
- const [activeTab, setActiveTab] = useState(initialTab);
11
- const [visibleTabs, setVisibleTabs] = useState<number[]>(
12
- Array.from({ length: totalTabs }, (_, i) => i),
13
- );
14
- const [overflowTabs, setOverflowTabs] = useState<number[]>([]);
15
- const [allTabsHidden, setAllTabsHidden] = useState<boolean>(false);
11
+ type Tabs = {
12
+ visible: number[];
13
+ overflow: number[];
14
+ };
15
+
16
+ const MORE_BUTTON_WIDTH = 80;
17
+ const TABS_GAP = 8;
18
+
19
+ export function useTabs({ activeTab, onTabChange, totalTabs, containerRef }: UseTabsProps) {
20
+ const [tabs, setTabs] = useState<Tabs>({
21
+ visible: Array.from({ length: totalTabs }, (_, i) => i),
22
+ overflow: [],
23
+ });
24
+
16
25
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
26
+ const tabWidthsRef = useRef<number[]>([]);
17
27
  const tabLabelsRef = useRef<string[]>([]);
18
- const resizeTimeoutRef = useRef<number | undefined>(undefined);
19
- const [ready, setReady] = useState<boolean>(false);
20
- const hasCalculatedOnce = useRef(false);
21
- const lastWidthRef = useRef<number>(0);
22
- const originalOrderRef = useRef<number[]>([]);
23
28
 
24
- useEffect(() => {
25
- originalOrderRef.current = Array.from({ length: totalTabs }, (_, i) => i);
26
- }, [totalTabs]);
29
+ const allTabsHidden = useMemo(() => tabs.visible.length === 0, [tabs.visible]);
27
30
 
28
31
  const setTabRef = useCallback((element: HTMLButtonElement | null, index: number) => {
29
32
  tabRefs.current[index] = element;
30
- if (element) {
31
- const label = element.getAttribute('data-label');
32
- if (label) {
33
- tabLabelsRef.current[index] = label;
34
- }
33
+
34
+ const width = element?.offsetWidth;
35
+ if (width) {
36
+ tabWidthsRef.current[index] = width;
35
37
  }
36
- }, []);
37
38
 
38
- const getTabId = useCallback((label: string, index: number) => {
39
- const cleanLabel = label.replace(/\s+/g, '-').toLowerCase();
40
- return `${cleanLabel}-${index}`;
39
+ const label = element?.getAttribute('data-label');
40
+ if (label) {
41
+ tabLabelsRef.current[index] = label;
42
+ }
41
43
  }, []);
42
44
 
43
45
  const focusTab = (index: number) => {
44
46
  const currentElement = tabRefs.current[index];
45
- if (currentElement) {
46
- currentElement.focus();
47
- }
47
+ currentElement?.focus();
48
48
  };
49
49
 
50
- const onTabSelect = useCallback((index: number) => {
51
- focusTab(index);
52
- const label = tabRefs.current[index]?.getAttribute('data-label');
53
- if (label) setActiveTab(label);
54
- }, []);
55
-
56
- const onTabClick = useCallback(
57
- (labelOrIndex: string | number) => {
58
- let clickedIndex: number;
59
-
60
- if (typeof labelOrIndex === 'string') {
61
- clickedIndex = tabRefs.current.findIndex(
62
- (ref) => ref?.getAttribute('data-label') === labelOrIndex,
63
- );
64
- if (clickedIndex === -1) return;
65
- } else {
66
- clickedIndex = labelOrIndex;
67
- }
68
-
69
- if (allTabsHidden) {
70
- const label = tabLabelsRef.current[clickedIndex];
71
- if (label) {
72
- setActiveTab(label);
73
- focusTab(clickedIndex);
74
- }
75
- return;
76
- }
77
-
78
- if (overflowTabs.includes(clickedIndex)) {
79
- const newVisibleTabs = [...visibleTabs];
80
- const newOverflowTabs = [...overflowTabs];
81
-
82
- const clickedIdxInOverflow = newOverflowTabs.indexOf(clickedIndex);
83
- if (clickedIdxInOverflow !== -1) {
84
- newOverflowTabs.splice(clickedIdxInOverflow, 1);
85
- }
86
-
87
- const lastVisible = newVisibleTabs.pop();
88
- if (lastVisible !== undefined) {
89
- newOverflowTabs.unshift(lastVisible);
90
- }
91
-
92
- newVisibleTabs.push(clickedIndex);
93
-
94
- setVisibleTabs(newVisibleTabs);
95
- setOverflowTabs(newOverflowTabs);
96
-
97
- requestAnimationFrame(() => {
98
- const label = tabRefs.current[clickedIndex]?.getAttribute('data-label');
99
- if (label) {
100
- setActiveTab(label);
101
- focusTab(clickedIndex);
102
- }
103
- });
104
- } else {
105
- const label = tabRefs.current[clickedIndex]?.getAttribute('data-label');
106
- if (label) {
107
- setActiveTab(label);
108
- focusTab(clickedIndex);
109
- }
110
- }
50
+ const onTabSelect = useCallback(
51
+ (index: number) => {
52
+ focusTab(index);
53
+ const label = tabRefs.current[index]?.getAttribute('data-label');
54
+ if (label) onTabChange(label);
111
55
  },
112
- [visibleTabs, overflowTabs, allTabsHidden],
56
+ [onTabChange],
113
57
  );
114
58
 
115
59
  const handleKeyboard = useCallback(
@@ -133,196 +77,174 @@ export function useTabs({ initialTab, totalTabs, containerRef }: UseTabsProps) {
133
77
  [totalTabs, onTabSelect],
134
78
  );
135
79
 
136
- const calculateVisibleTabs = useCallback(() => {
137
- const container = containerRef?.current;
138
- if (!container) return;
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
+ }
139
96
 
140
- const contentWrapper = container.closest('div');
141
- if (!contentWrapper) {
142
- setVisibleTabs(Array.from({ length: totalTabs }, (_, i) => i));
143
- setOverflowTabs([]);
144
- setAllTabsHidden(false);
145
- return;
146
- }
97
+ setTabs({
98
+ visible: newVisibleTabs,
99
+ overflow: newOverflowTabs,
100
+ });
101
+ },
102
+ [tabs],
103
+ );
147
104
 
148
- const containerWidth = container.offsetWidth - 60;
149
- const tabElements = container.querySelectorAll('[role="tab"]');
150
- const moreButtonWidth = 80;
151
- const safetyMargin = 20;
105
+ const onTabClick = useCallback(
106
+ (labelOrIndex: string | number) => {
107
+ const clickedIndex =
108
+ typeof labelOrIndex === 'string'
109
+ ? tabRefs.current.findIndex((ref) => ref?.getAttribute('data-label') === labelOrIndex)
110
+ : labelOrIndex;
111
+
112
+ if (clickedIndex === -1) return;
113
+
114
+ const hasOverflowTabs = tabs.overflow.length > 0;
115
+ if (hasOverflowTabs && !allTabsHidden && tabs.overflow.includes(clickedIndex)) {
116
+ replaceLastVisibleTabWithClickedOverflowTab(clickedIndex);
117
+ }
152
118
 
153
- const tabWidths = Array.from(tabElements).map((el) => (el as HTMLElement).offsetWidth);
154
- const tabLabels = Array.from(tabElements).map((el) => el.getAttribute('data-label') || '');
155
- const tabTypes = Array.from(tabElements).map((el) => el.getAttribute('data-type') || '');
119
+ const label = tabLabelsRef.current[clickedIndex];
120
+ if (label) {
121
+ onTabChange(label);
122
+ focusTab(clickedIndex);
123
+ }
124
+ },
125
+ [allTabsHidden, tabs.overflow, onTabChange, replaceLastVisibleTabWithClickedOverflowTab],
126
+ );
156
127
 
157
- const hasLongLabels = tabLabels.some((label) => label.length > 30);
158
- const minVisibleTabs = hasLongLabels ? 1 : 2;
128
+ const calculateVisibleTabs = useCallback(() => {
129
+ const container = containerRef?.current;
130
+ if (!container) return;
159
131
 
132
+ const containerWidth = container.offsetWidth;
133
+ const tabWidths = tabWidthsRef.current;
160
134
  const activeTabIndex = tabRefs.current.findIndex(
161
135
  (ref) => ref?.getAttribute('data-label') === activeTab,
162
136
  );
163
137
 
164
- let currentWidth = 0;
165
- const visible: number[] = [];
166
- const overflow: number[] = [];
138
+ // Active tab should always be visible, so we include it at the beginning of the array
139
+ let tabsWidth = activeTabIndex !== -1 ? tabWidths[activeTabIndex] : 0;
140
+ const visible = activeTabIndex !== -1 ? [activeTabIndex] : [];
141
+ const overflow = [];
167
142
 
168
- let minTabsWidth = 0;
169
- Array.from({ length: minVisibleTabs }).forEach((_, i) => {
170
- if (i < tabWidths.length) {
171
- minTabsWidth += tabWidths[i] + (i > 0 ? moreButtonWidth + safetyMargin : 0);
143
+ for (let i = 0; i < tabWidths.length; i++) {
144
+ // Skip active tab, it was added initially
145
+ if (i === activeTabIndex) {
146
+ continue;
172
147
  }
173
- });
174
148
 
175
- if (minTabsWidth > containerWidth) {
176
- setVisibleTabs([]);
177
- setOverflowTabs(Array.from({ length: totalTabs }, (_, i) => i));
178
- setAllTabsHidden(true);
179
- return;
180
- }
149
+ const tabWidthWithGap = tabWidths[i] + TABS_GAP;
150
+ const projectedWidth = tabsWidth + tabWidthWithGap;
181
151
 
182
- const tabsByType = new Map<string, number[]>();
183
- Array.from({ length: totalTabs }).forEach((_, i) => {
184
- const type = tabTypes[i] || 'default';
185
- if (!tabsByType.has(type)) {
186
- tabsByType.set(type, []);
152
+ if (projectedWidth <= containerWidth) {
153
+ visible.push(i);
154
+ tabsWidth += tabWidthWithGap;
155
+ } else {
156
+ overflow.push(i);
187
157
  }
188
- tabsByType.get(type)?.push(i);
189
- });
190
-
191
- tabsByType.forEach((tabIndices) => {
192
- let typeCurrentWidth = currentWidth;
193
- const typeVisible: number[] = [];
194
- const typeOverflow: number[] = [];
195
-
196
- tabIndices.slice(0, minVisibleTabs).forEach((tabIndex) => {
197
- const tabWidth = tabWidths[tabIndex];
198
- const projectedWidth =
199
- typeCurrentWidth +
200
- tabWidth +
201
- (typeVisible.length > 0 ? moreButtonWidth + safetyMargin : 0);
202
-
203
- if (projectedWidth <= containerWidth) {
204
- typeVisible.push(tabIndex);
205
- typeCurrentWidth += tabWidth;
206
- } else {
207
- typeOverflow.push(tabIndex);
208
- }
209
- });
210
-
211
- tabIndices.slice(minVisibleTabs).forEach((tabIndex) => {
212
- const tabWidth = tabWidths[tabIndex];
213
- const projectedWidth = typeCurrentWidth + tabWidth + moreButtonWidth + safetyMargin;
214
-
215
- if (projectedWidth <= containerWidth) {
216
- typeVisible.push(tabIndex);
217
- typeCurrentWidth += tabWidth;
218
- } else {
219
- typeOverflow.push(tabIndex);
220
- }
221
- });
158
+ }
222
159
 
223
- visible.push(...typeVisible);
224
- overflow.push(...typeOverflow);
225
- currentWidth = typeCurrentWidth;
226
- });
160
+ if (overflow.length > 0) {
161
+ tabsWidth += MORE_BUTTON_WIDTH;
227
162
 
228
- if (activeTabIndex !== -1 && !visible.includes(activeTabIndex)) {
229
- if (visible.length > 0) {
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) {
230
165
  const removed = visible.pop();
231
166
  if (removed !== undefined) {
232
167
  overflow.unshift(removed);
168
+ tabsWidth -= tabWidths[removed];
233
169
  }
234
170
  }
235
-
236
- visible.push(activeTabIndex);
237
- const activeOverflowIndex = overflow.indexOf(activeTabIndex);
238
- if (activeOverflowIndex !== -1) overflow.splice(activeOverflowIndex, 1);
239
171
  }
240
172
 
241
- setVisibleTabs(visible);
242
- setOverflowTabs(overflow);
243
- setAllTabsHidden(visible.length === 0);
244
- // eslint-disable-next-line react-hooks/exhaustive-deps
245
- }, [containerRef, totalTabs]);
173
+ setTabs({
174
+ visible,
175
+ overflow,
176
+ });
177
+ }, [containerRef, activeTab]);
246
178
 
247
179
  useEffect(() => {
248
180
  if (!containerRef?.current) return;
249
181
 
250
- const ensureTabsReady = () => {
251
- const allTabsReady =
252
- tabRefs.current.length === totalTabs && tabRefs.current.every((tab) => tab?.offsetWidth);
253
-
254
- if (!allTabsReady) {
255
- resizeTimeoutRef.current = requestAnimationFrame(ensureTabsReady);
256
- return;
257
- }
258
-
259
- calculateVisibleTabs();
260
- hasCalculatedOnce.current = true;
261
- };
262
-
263
- resizeTimeoutRef.current = requestAnimationFrame(ensureTabsReady);
264
-
265
- let resizeTimeout: number;
182
+ let resizeTimeout = requestAnimationFrame(calculateVisibleTabs);
266
183
  const handleResize = () => {
267
- if (!hasCalculatedOnce.current) return;
268
-
269
184
  if (resizeTimeout) {
270
185
  cancelAnimationFrame(resizeTimeout);
271
186
  }
272
-
273
- resizeTimeout = requestAnimationFrame(() => {
274
- if (resizeTimeoutRef.current) {
275
- cancelAnimationFrame(resizeTimeoutRef.current);
276
- }
277
- resizeTimeoutRef.current = requestAnimationFrame(() => {
278
- const container = containerRef?.current;
279
- if (!container) return;
280
-
281
- const currentWidth = container.offsetWidth;
282
-
283
- if (Math.abs(lastWidthRef.current - currentWidth) > 5) {
284
- lastWidthRef.current = currentWidth;
285
- calculateVisibleTabs();
286
- }
287
- });
288
- });
187
+ resizeTimeout = requestAnimationFrame(calculateVisibleTabs);
289
188
  };
290
189
 
291
- const resizeObserver = new ResizeObserver(handleResize);
292
- resizeObserver.observe(containerRef.current);
293
190
  window.addEventListener('resize', handleResize);
294
-
295
191
  return () => {
296
- resizeObserver.disconnect();
297
192
  window.removeEventListener('resize', handleResize);
298
- if (resizeTimeoutRef.current) {
299
- cancelAnimationFrame(resizeTimeoutRef.current);
300
- }
301
- if (resizeTimeout) {
302
- cancelAnimationFrame(resizeTimeout);
303
- }
193
+ cancelAnimationFrame(resizeTimeout);
304
194
  };
305
195
  }, [containerRef, totalTabs, calculateVisibleTabs]);
306
196
 
307
- useEffect(() => {
308
- const raf = requestAnimationFrame(() => {
309
- setReady(true);
310
- calculateVisibleTabs();
311
- });
312
-
313
- return () => cancelAnimationFrame(raf);
314
- }, [calculateVisibleTabs]);
315
-
316
197
  return {
317
- activeTab,
318
- setActiveTab,
319
198
  setTabRef,
320
199
  onTabClick,
321
200
  handleKeyboard,
322
- getTabId,
323
- visibleTabs,
324
- overflowTabs,
325
- ready,
201
+ visibleTabs: tabs.visible,
202
+ overflowTabs: tabs.overflow,
326
203
  allTabsHidden,
327
204
  };
328
205
  }
206
+
207
+ type UseActiveTabProps = {
208
+ initialTab: string;
209
+ tabsId?: string;
210
+ };
211
+
212
+ export const useActiveTab = ({ initialTab, tabsId }: UseActiveTabProps) => {
213
+ const [searchParams, setSearchParams] = useSearchParams();
214
+ const [activeTab, setActiveTab] = useState(getInitialTab({ initialTab, searchParams, tabsId }));
215
+ const prevActiveTabRef = useRef(activeTab);
216
+
217
+ useEffect(() => {
218
+ const hasActiveTabChanged = prevActiveTabRef.current !== activeTab;
219
+ if (!tabsId || !hasActiveTabChanged) {
220
+ return;
221
+ }
222
+
223
+ prevActiveTabRef.current = activeTab;
224
+
225
+ setSearchParams((searchParams) => {
226
+ searchParams.set(tabsId, activeTab);
227
+ return searchParams;
228
+ });
229
+ }, [activeTab, setSearchParams, tabsId]);
230
+
231
+ return {
232
+ activeTab,
233
+ setActiveTab,
234
+ };
235
+ };
236
+
237
+ type GetInitialTabProps = {
238
+ initialTab: string;
239
+ searchParams: URLSearchParams;
240
+ tabsId?: string;
241
+ };
242
+
243
+ const getInitialTab = ({ initialTab, searchParams, tabsId }: GetInitialTabProps) => {
244
+ let resultTab = initialTab;
245
+ if (tabsId) {
246
+ const tabFromUrl = searchParams.get(tabsId);
247
+ resultTab = tabFromUrl ? tabFromUrl : resultTab;
248
+ }
249
+ return resultTab;
250
+ };
@@ -17,6 +17,7 @@ export {
17
17
  addTrailingSlash,
18
18
  withPathPrefix,
19
19
  } from '../utils/urls';
20
+ export { capitalize } from '../utils/string';
20
21
  export { typedMemo } from '../hoc/typedMemo';
21
22
  export { useMount } from '../hooks/use-mount';
22
23
  export { GlobalStyle } from '../styles/global';
@@ -206,6 +206,9 @@ export type TranslationKey =
206
206
  | 'page.actions.claudeTitle'
207
207
  | 'page.actions.claudeButtonText'
208
208
  | 'page.actions.claudeDescription'
209
+ | 'page.actions.cursorMcpButtonText'
210
+ | 'page.actions.cursorMcpTitle'
211
+ | 'page.actions.cursorMcpDescription'
209
212
  | 'openapi.download.description.title'
210
213
  | 'openapi.info.title'
211
214
  | 'openapi.info.contact.url'
@@ -283,6 +286,16 @@ export type TranslationKey =
283
286
  | 'openapi.schemaCatalogLink.title'
284
287
  | 'openapi.schemaCatalogLink.copyButtonTooltip'
285
288
  | 'openapi.schemaCatalogLink.copiedTooltip'
289
+ | 'openapi.mcp.title'
290
+ | 'openapi.mcp.endpoint'
291
+ | 'openapi.mcp.tools'
292
+ | 'openapi.mcp.protocolVersion'
293
+ | 'openapi.mcp.capabilities'
294
+ | 'openapi.mcp.experimentalCapabilities'
295
+ | 'openapi.mcp.inputSchema'
296
+ | 'openapi.mcp.inputExample'
297
+ | 'openapi.mcp.outputSchema'
298
+ | 'openapi.mcp.outputExample'
286
299
  | 'asyncapi.download.description.title'
287
300
  | 'asyncapi.info.title'
288
301
  | 'graphql.queries'
@@ -1,6 +1,7 @@
1
1
  export type OpenAPIServer = {
2
2
  url: string;
3
3
  description?: string;
4
+ name?: string;
4
5
  variables?: Record<
5
6
  string,
6
7
  {
@@ -37,3 +37,4 @@ export * from './lang-to-name';
37
37
  export * from './enhanced-smoothstep';
38
38
  export * from './icon-resolver';
39
39
  export * from './dynamic';
40
+ export * from './tabs';
@@ -0,0 +1,4 @@
1
+ export function getTabId(label: string, index: number) {
2
+ const cleanLabel = label.replace(/\s+/g, '-').toLowerCase();
3
+ return `${cleanLabel}-${index}`;
4
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ const Icon = (props: IconProps) => (
7
+ <svg
8
+ width="16"
9
+ height="16"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ {...props}
14
+ >
15
+ <path d="M7.99956 15V8.0001L2 11.4999L7.99956 15Z" fill="#939393" />
16
+ <path d="M14 4.49979L7.99956 15V8.0001L14 4.49979Z" fill="#E3E3E3" />
17
+ <path d="M2 4.49979H14L7.99956 8.0001L2 4.49979Z" fill="white" />
18
+ <path d="M8.00025 1V4.49995L14 4.49979L8.00025 1Z" fill="#444444" />
19
+ <path
20
+ d="M2 4.49979L8.00025 4.49995V1L2 4.49979ZM13.9999 11.4998L10.9999 9.74987L7.99956 15L13.9999 11.4998Z"
21
+ fill="#939393"
22
+ />
23
+ <path
24
+ d="M14 4.49979L10.9999 9.74987L13.9999 11.4998L14 4.49979ZM7.99956 8.0001L2 11.4999V4.49979L7.99956 8.0001Z"
25
+ fill="#444444"
26
+ />
27
+ </svg>
28
+ );
29
+
30
+ export const CursorIcon = styled(Icon).attrs(() => ({
31
+ 'data-component-name': 'icons/CursorIcon/CursorIcon',
32
+ }))<IconProps>`
33
+ height: ${({ size }) => size || '16px'};
34
+ width: ${({ size }) => size || '16px'};
35
+ `;
@@ -9,12 +9,7 @@ import { breakpoints } from '@redocly/theme/core/utils';
9
9
  import { PageNavigation } from '@redocly/theme/components/PageNavigation/PageNavigation';
10
10
  import { LastUpdated } from '@redocly/theme/components/LastUpdated/LastUpdated';
11
11
  import { Breadcrumbs as ThemeBreadcrumbs } from '@redocly/theme/components/Breadcrumbs/Breadcrumbs';
12
-
13
- import {
14
- CodeSnippetContext,
15
- CODE_GROUP_SNIPPET_NAME_KEY,
16
- } from '../core/contexts/CodeSnippetContext';
17
- import { useLocalState } from '../core/hooks/use-local-state';
12
+ import { CodeSnippetProvider } from '@redocly/theme/core/contexts/CodeSnippetContext';
18
13
 
19
14
  type DocumentationLayoutProps = {
20
15
  tableOfContent: React.ReactNode;
@@ -44,10 +39,8 @@ export function DocumentationLayout({
44
39
  const { editPage: themeEditPage } = config || {};
45
40
  const mergedConf = editPage ? { ...themeEditPage, ...editPage } : undefined;
46
41
 
47
- const [activeSnippetName, setActiveSnippetName] = useLocalState(CODE_GROUP_SNIPPET_NAME_KEY, '');
48
-
49
42
  return (
50
- <CodeSnippetContext.Provider value={{ activeSnippetName, setActiveSnippetName }}>
43
+ <CodeSnippetProvider>
51
44
  <LayoutWrapper data-component-name="Layout/DocumentationLayout" className={className}>
52
45
  <ContentWrapper withToc={!config?.toc?.hide}>
53
46
  <Breadcrumbs />
@@ -61,7 +54,7 @@ export function DocumentationLayout({
61
54
  </ContentWrapper>
62
55
  {tableOfContent}
63
56
  </LayoutWrapper>
64
- </CodeSnippetContext.Provider>
57
+ </CodeSnippetProvider>
65
58
  );
66
59
  }
67
60