@redocly/theme 0.58.1 → 0.59.0-next.1

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 (104) hide show
  1. package/lib/components/Accordion/Accordion.d.ts +12 -0
  2. package/lib/components/Accordion/Accordion.js +75 -0
  3. package/lib/components/Accordion/AccordionBody.d.ts +8 -0
  4. package/lib/components/Accordion/AccordionBody.js +63 -0
  5. package/lib/components/Accordion/AccordionHeader.d.ts +10 -0
  6. package/lib/components/Accordion/AccordionHeader.js +37 -0
  7. package/lib/components/Accordion/AccordionTitle.d.ts +6 -0
  8. package/lib/components/Accordion/AccordionTitle.js +20 -0
  9. package/lib/components/Accordion/variables.d.ts +1 -0
  10. package/lib/components/Accordion/variables.js +59 -0
  11. package/lib/components/Buttons/AIAssistantButton.d.ts +2 -0
  12. package/lib/components/Buttons/AIAssistantButton.js +125 -0
  13. package/lib/components/Buttons/variables.d.ts +1 -0
  14. package/lib/components/Buttons/variables.dark.d.ts +1 -0
  15. package/lib/components/Buttons/variables.dark.js +10 -0
  16. package/lib/components/Buttons/variables.js +51 -0
  17. package/lib/components/Catalog/Catalog.js +3 -3
  18. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  19. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  20. package/lib/components/Catalog/CatalogFilter/CatalogFilter.d.ts +6 -0
  21. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +35 -0
  22. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.d.ts +6 -0
  23. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +142 -0
  24. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.d.ts +13 -0
  25. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.js +92 -0
  26. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.d.ts +6 -0
  27. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +111 -0
  28. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.d.ts +6 -0
  29. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +116 -0
  30. package/lib/components/Catalog/CatalogSelector.js +0 -1
  31. package/lib/components/Catalog/variables.js +0 -1
  32. package/lib/components/Filter/FilterInput.d.ts +1 -0
  33. package/lib/components/Filter/FilterInput.js +2 -2
  34. package/lib/components/Filter/FilterOptions.js +2 -0
  35. package/lib/components/Filter/variables.js +7 -4
  36. package/lib/components/Search/SearchAiDialog.js +2 -3
  37. package/lib/components/Search/SearchAiResponse.js +2 -3
  38. package/lib/components/Search/SearchDialog.d.ts +2 -1
  39. package/lib/components/Search/SearchDialog.js +2 -2
  40. package/lib/components/Tag/Tag.d.ts +3 -2
  41. package/lib/components/Tag/Tag.js +21 -5
  42. package/lib/components/Tag/variables.dark.js +135 -0
  43. package/lib/components/Tag/variables.js +120 -58
  44. package/lib/core/hooks/use-tabs.d.ts +11 -6
  45. package/lib/core/hooks/use-tabs.js +117 -207
  46. package/lib/core/styles/dark.js +29 -26
  47. package/lib/core/styles/global.js +64 -59
  48. package/lib/core/types/l10n.d.ts +1 -1
  49. package/lib/core/utils/index.d.ts +1 -0
  50. package/lib/core/utils/index.js +1 -0
  51. package/lib/core/utils/tabs.d.ts +1 -0
  52. package/lib/core/utils/tabs.js +8 -0
  53. package/lib/icons/RedoclyIcon/RedoclyIcon.d.ts +9 -0
  54. package/lib/icons/RedoclyIcon/RedoclyIcon.js +27 -0
  55. package/lib/index.d.ts +2 -0
  56. package/lib/index.js +2 -0
  57. package/lib/layouts/RootLayout.js +6 -1
  58. package/lib/markdoc/components/Tabs/Tab.js +1 -1
  59. package/lib/markdoc/components/Tabs/TabList.d.ts +2 -14
  60. package/lib/markdoc/components/Tabs/TabList.js +63 -16
  61. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -2
  62. package/lib/markdoc/components/Tabs/Tabs.js +11 -87
  63. package/lib/markdoc/tags/tabs.js +5 -0
  64. package/package.json +2 -2
  65. package/src/components/Accordion/Accordion.tsx +100 -0
  66. package/src/components/Accordion/AccordionBody.tsx +65 -0
  67. package/src/components/Accordion/AccordionHeader.tsx +68 -0
  68. package/src/components/Accordion/AccordionTitle.tsx +26 -0
  69. package/src/components/Accordion/variables.ts +56 -0
  70. package/src/components/Buttons/AIAssistantButton.tsx +141 -0
  71. package/src/components/Buttons/variables.dark.ts +7 -0
  72. package/src/components/Buttons/variables.ts +48 -0
  73. package/src/components/Catalog/Catalog.tsx +3 -2
  74. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  75. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  76. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +56 -0
  77. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +169 -0
  78. package/src/components/Catalog/CatalogFilter/CatalogFilterContent.tsx +121 -0
  79. package/src/components/Catalog/CatalogFilter/CatalogFilterDateRange.tsx +147 -0
  80. package/src/components/Catalog/CatalogFilter/CatalogFilterSelect.tsx +136 -0
  81. package/src/components/Catalog/CatalogSelector.tsx +0 -1
  82. package/src/components/Catalog/variables.ts +0 -1
  83. package/src/components/Filter/FilterInput.tsx +3 -2
  84. package/src/components/Filter/FilterOptions.tsx +2 -0
  85. package/src/components/Filter/variables.ts +7 -4
  86. package/src/components/Search/SearchAiDialog.tsx +2 -2
  87. package/src/components/Search/SearchAiResponse.tsx +2 -2
  88. package/src/components/Search/SearchDialog.tsx +7 -2
  89. package/src/components/Tag/Tag.tsx +33 -8
  90. package/src/components/Tag/variables.dark.ts +135 -0
  91. package/src/components/Tag/variables.ts +120 -58
  92. package/src/core/hooks/use-tabs.ts +160 -238
  93. package/src/core/styles/dark.ts +11 -8
  94. package/src/core/styles/global.ts +7 -2
  95. package/src/core/types/l10n.ts +1 -0
  96. package/src/core/utils/index.ts +1 -0
  97. package/src/core/utils/tabs.ts +4 -0
  98. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +44 -0
  99. package/src/index.ts +2 -0
  100. package/src/layouts/RootLayout.tsx +6 -0
  101. package/src/markdoc/components/Tabs/Tab.tsx +1 -0
  102. package/src/markdoc/components/Tabs/TabList.tsx +84 -30
  103. package/src/markdoc/components/Tabs/Tabs.tsx +12 -125
  104. 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
+ };
@@ -4,6 +4,7 @@ import { scorecardDarkMode } from '@redocly/theme/components/Scorecard/variables
4
4
  import { mermaidDarkMode } from '@redocly/theme/markdoc/components/Mermaid/variables.dark'
5
5
  import { menuDarkMode } from '@redocly/theme/components/Menu/variables.dark';
6
6
  import { buttonDarkMode } from '@redocly/theme/components/Button/variables.dark';
7
+ import { aiAssistantButtonDarkMode } from '@redocly/theme/components/Buttons/variables.dark';
7
8
  import { segmentedButtonsDarkMode } from '@redocly/theme/components/Segmented/variables.dark';
8
9
  import { checkboxDarkMode } from '@redocly/theme/icons/CheckboxIcon/variables.dark';
9
10
  import { tagDarkMode } from '@redocly/theme/components/Tag/variables.dark';
@@ -137,14 +138,15 @@ export const darkMode = css`
137
138
 
138
139
  --color-green-1: #1a4d40;
139
140
  --color-green-2: #195848;
140
- --color-green-3: #0e8450;
141
- --color-green-4: #149e53;
142
- --color-green-5: #1cb854;
143
- --color-green-6: #4dd470;
144
- --color-green-7: #72e985;
145
- --color-green-8: #a3f7a9;
146
- --color-green-9: #d2fbd0;
147
- --color-green-10: #edfbec;
141
+ --color-green-3: #136a4d;
142
+ --color-green-4: #0e8450;
143
+ --color-green-5: #149e53;
144
+ --color-green-6: #1cb854;
145
+ --color-green-7: #4dd470;
146
+ --color-green-8: #72e985;
147
+ --color-green-9: #a3f7a9;
148
+ --color-green-10: #d2fbd0;
149
+ --color-green-11: #edfbec;
148
150
 
149
151
  --color-grass-1: #1f4d2d;
150
152
  --color-grass-2: #164f29;
@@ -309,6 +311,7 @@ export const darkMode = css`
309
311
 
310
312
  ${segmentedButtonsDarkMode}
311
313
  ${buttonDarkMode}
314
+ ${aiAssistantButtonDarkMode}
312
315
  ${checkboxDarkMode}
313
316
  ${tagDarkMode}
314
317
  ${statusCodeDarkMode}
@@ -11,6 +11,7 @@ import { catalog } from '@redocly/theme/components/Catalog/variables';
11
11
  import { filter } from '@redocly/theme/components/Filter/variables';
12
12
  import { catalogClassic } from '@redocly/theme/components/CatalogClassic/variables';
13
13
  import { apiReferencePanels, responsePanelColors } from '@redocly/theme/components/Panel/variables';
14
+ import { accordion } from '@redocly/theme/components/Accordion/variables';
14
15
  import { select } from '@redocly/theme/components/Select/variables';
15
16
  import { dropdown } from '@redocly/theme/components/Dropdown/variables';
16
17
  import { tooltip } from '@redocly/theme/components/Tooltip/variables';
@@ -18,6 +19,7 @@ import { checkbox } from '@redocly/theme/icons/CheckboxIcon/variables';
18
19
  import { admonition } from '@redocly/theme/components/Admonition/variables';
19
20
  import { footer } from '@redocly/theme/components/Footer/variables';
20
21
  import { button } from '@redocly/theme/components/Button/variables';
22
+ import { aiAssistantButton } from '@redocly/theme/components/Buttons/variables';
21
23
  import { navbar } from '@redocly/theme/components/Navbar/variables';
22
24
  import { search } from '@redocly/theme/components/Search/variables';
23
25
  import { menu, mobileMenu } from '@redocly/theme/components/Menu/variables';
@@ -141,8 +143,9 @@ const themeColors = css`
141
143
  --color-green-6: #1cb854;
142
144
  --color-green-7: #149e53;
143
145
  --color-green-8: #0e8450;
144
- --color-green-9: #195848;
145
- --color-green-10: #1a4d40;
146
+ --color-green-9: #136a4d;
147
+ --color-green-10: #195848;
148
+ --color-green-11: #1a4d40;
146
149
 
147
150
  --color-grass-1: #f0faeb;
148
151
  --color-grass-2: #e3fad6;
@@ -1227,6 +1230,7 @@ const replay = css`
1227
1230
 
1228
1231
  export const styles = css`
1229
1232
  :root {
1233
+ ${accordion}
1230
1234
  ${admonition}
1231
1235
  ${apiReferenceDocs}
1232
1236
  ${apiReferencePanels}
@@ -1234,6 +1238,7 @@ export const styles = css`
1234
1238
  ${borders}
1235
1239
  ${breadcrumbs}
1236
1240
  ${button}
1241
+ ${aiAssistantButton}
1237
1242
  ${cards}
1238
1243
  ${catalog}
1239
1244
  ${catalogClassic}
@@ -103,6 +103,7 @@ export type TranslationKey =
103
103
  | 'search.ai.error.header'
104
104
  | 'search.ai.error.header.forbidden'
105
105
  | 'search.ai.error.header.unauthorized'
106
+ | 'aiAssistant.trigger'
106
107
  | 'toc.header'
107
108
  | 'footer.copyrightText'
108
109
  | 'page.homeButton'
@@ -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,44 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ import { getCssColorVariable } from '@redocly/theme/core/utils';
7
+
8
+ const Icon = (props: IconProps) => (
9
+ <svg
10
+ width="18"
11
+ height="18"
12
+ viewBox="0 0 18 18"
13
+ fill="none"
14
+ xmlns="http://www.w3.org/2000/svg"
15
+ {...props}
16
+ >
17
+ <g clipPath="url(#clip0_4053_1165)">
18
+ <path
19
+ d="M14.625 6.19973C14.625 8.69176 12.6173 10.712 10.1406 10.712H2.8125V10.2717C5.04753 10.2717 6.85938 8.44864 6.85938 6.19973C6.85938 3.95082 5.04753 2.12772 2.8125 2.12772V1.6875H10.1406C12.6173 1.6875 14.625 3.70769 14.625 6.19973Z"
20
+ fill="#2467F2"
21
+ />
22
+ <path
23
+ d="M14.625 16.875C14.625 14.383 12.6173 12.3628 10.1406 12.3628H2.8125V12.803C5.04753 12.803 6.85938 14.6261 6.85938 16.875H14.625Z"
24
+ fill="#2467F2"
25
+ />
26
+ </g>
27
+ <defs>
28
+ <clipPath id="clip0_4053_1165">
29
+ <rect width="18" height="18" fill="white" />
30
+ </clipPath>
31
+ </defs>
32
+ </svg>
33
+ );
34
+
35
+ export const RedoclyIcon = styled(Icon).attrs(() => ({
36
+ 'data-component-name': 'icons/RedoclyIcon/RedoclyIcon',
37
+ }))<IconProps>`
38
+ path {
39
+ fill: ${({ color }) => getCssColorVariable(color)};
40
+ }
41
+
42
+ height: ${({ size }) => size || '16px'};
43
+ width: ${({ size }) => size || '16px'};
44
+ `;
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export * from '@redocly/theme/components/Buttons/CopyButton';
32
32
  export * from '@redocly/theme/components/Buttons/EditPageButton';
33
33
  export * from '@redocly/theme/components/Buttons/EmailButton';
34
34
  export * from '@redocly/theme/components/Buttons/NewTabButton';
35
+ export * from '@redocly/theme/components/Buttons/AIAssistantButton';
35
36
  /* Markdown */
36
37
  export * from '@redocly/theme/components/Markdown/Markdown';
37
38
  export * from '@redocly/theme/components/Markdown/styles/baseTable';
@@ -282,6 +283,7 @@ export * from '@redocly/theme/icons/RabbitMQIcon/RabbitMQIcon';
282
283
  export * from '@redocly/theme/icons/CurveAutoColonIcon/CurveAutoColonIcon';
283
284
  export * from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon';
284
285
  export * from '@redocly/theme/icons/AiStarsGradientIcon/AiStarsGradientIcon';
286
+ export * from '@redocly/theme/icons/RedoclyIcon/RedoclyIcon';
285
287
  export * from '@redocly/theme/icons/WorkflowHierarchyIcon/WorkflowHierarchyIcon';
286
288
  export * from '@redocly/theme/icons/GenericIcon/GenericIcon';
287
289
  export * from '@redocly/theme/icons/ShareIcon/ShareIcon';
@@ -5,18 +5,24 @@ import type { JSX } from 'react';
5
5
  import { Navbar } from '@redocly/theme/components/Navbar/Navbar';
6
6
  import { Footer } from '@redocly/theme/components/Footer/Footer';
7
7
  import { SkipContent } from '@redocly/theme/components/SkipContent/SkipContent';
8
+ import { AIAssistantButton } from '@redocly/theme/components/Buttons/AIAssistantButton';
9
+ import { useThemeHooks } from '@redocly/theme/core/hooks';
8
10
 
9
11
  export type LayoutConfig = {
10
12
  children: React.ReactNode;
11
13
  };
12
14
 
13
15
  export function RootLayout({ children }: LayoutConfig): JSX.Element {
16
+ const { useSearch } = useThemeHooks();
17
+ const { askAi } = useSearch();
18
+
14
19
  return (
15
20
  <div data-component-name="layouts/RootLayout">
16
21
  <SkipContent />
17
22
  <Navbar />
18
23
  {children}
19
24
  <Footer />
25
+ {askAi && <AIAssistantButton />}
20
26
  </div>
21
27
  );
22
28
  }
@@ -34,6 +34,7 @@ export function TabComponent({
34
34
  <TabItem data-component-name="Markdoc/Tabs/Tab" size={size} tabIndex={0}>
35
35
  <TabButtonLink
36
36
  id={`tab-${tabId}`}
37
+ data-label={label}
37
38
  role="tab"
38
39
  aria-selected="false"
39
40
  aria-controls={`panel-${tabId}`}