@redocly/theme 0.59.0-next.1 → 0.59.0-next.2

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 (139) hide show
  1. package/lib/components/Accordion/Accordion.js +17 -7
  2. package/lib/components/Accordion/AccordionBody.js +17 -7
  3. package/lib/components/Admonition/Admonition.js +17 -7
  4. package/lib/components/Badge/Badge.js +17 -7
  5. package/lib/components/Breadcrumbs/Breadcrumb.js +17 -7
  6. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +17 -7
  7. package/lib/components/Button/Button.js +17 -7
  8. package/lib/components/Buttons/AIAssistantButton.js +17 -7
  9. package/lib/components/Buttons/CopyButton.js +17 -7
  10. package/lib/components/Catalog/Catalog.d.ts +6 -0
  11. package/lib/components/Catalog/Catalog.js +7 -6
  12. package/lib/components/Catalog/CatalogEntities.js +17 -7
  13. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.js +17 -7
  14. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.js +17 -7
  15. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +17 -7
  16. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  17. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +17 -7
  18. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  19. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +17 -7
  20. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +17 -7
  21. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.js +17 -7
  22. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +17 -7
  23. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +17 -7
  24. package/lib/components/Catalog/CatalogSortButton.js +17 -7
  25. package/lib/components/Catalog/CatalogTableView/CatalogTableHeaderCell.js +17 -7
  26. package/lib/components/Catalog/CatalogViewModeToggle.js +17 -7
  27. package/lib/components/CatalogClassic/CatalogClassicActions.js +17 -7
  28. package/lib/components/CatalogClassic/CatalogClassicCard.js +17 -7
  29. package/lib/components/CatalogClassic/CatalogClassicHighlight.js +17 -7
  30. package/lib/components/CatalogClassic/CatalogClassicVirtualizedGroups.js +17 -7
  31. package/lib/components/CodeBlock/CodeBlock.js +17 -7
  32. package/lib/components/CodeBlock/CodeBlockContainer.js +17 -7
  33. package/lib/components/CodeBlock/CodeBlockTabs.js +17 -7
  34. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  35. package/lib/components/Dropdown/Dropdown.js +22 -12
  36. package/lib/components/Dropdown/DropdownMenuItem.js +17 -7
  37. package/lib/components/Feedback/Comment.js +17 -7
  38. package/lib/components/Feedback/Feedback.js +17 -7
  39. package/lib/components/Feedback/Mood.js +17 -7
  40. package/lib/components/Feedback/Rating.js +17 -7
  41. package/lib/components/Feedback/Reasons.js +17 -7
  42. package/lib/components/Feedback/Scale.js +17 -7
  43. package/lib/components/Feedback/Sentiment.js +17 -7
  44. package/lib/components/Feedback/Stars.js +17 -7
  45. package/lib/components/Filter/FilterContent.js +17 -7
  46. package/lib/components/Filter/FilterInput.js +17 -7
  47. package/lib/components/Image/Image.js +17 -7
  48. package/lib/components/JsonViewer/JsonViewer.js +17 -7
  49. package/lib/components/JsonViewer/helpers.js +17 -7
  50. package/lib/components/LastUpdated/LastUpdated.js +17 -7
  51. package/lib/components/Link/Link.js +17 -7
  52. package/lib/components/Markdown/Markdown.js +17 -7
  53. package/lib/components/Marker/Marker.js +17 -7
  54. package/lib/components/Menu/MenuContainer.js +17 -7
  55. package/lib/components/Menu/MenuItem.js +18 -8
  56. package/lib/components/Menu/MenuMobile.js +17 -7
  57. package/lib/components/PageActions/PageActions.js +17 -7
  58. package/lib/components/PageNavigation/NextButton.js +17 -7
  59. package/lib/components/Panel/Panel.js +17 -7
  60. package/lib/components/Panel/PanelBody.js +17 -7
  61. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +17 -7
  62. package/lib/components/Search/SearchAiConversationInput.d.ts +2 -1
  63. package/lib/components/Search/SearchAiConversationInput.js +28 -10
  64. package/lib/components/Search/SearchAiDialog.js +17 -7
  65. package/lib/components/Search/SearchDialog.js +23 -10
  66. package/lib/components/Search/SearchFilter.js +17 -7
  67. package/lib/components/Search/SearchGroups.js +17 -7
  68. package/lib/components/Search/SearchHighlight.js +17 -7
  69. package/lib/components/Search/SearchItem.js +17 -7
  70. package/lib/components/Search/SearchRecent.js +17 -7
  71. package/lib/components/Search/SearchShortcut.js +17 -7
  72. package/lib/components/Search/SearchSuggestedPages.js +17 -7
  73. package/lib/components/Search/SearchTrigger.js +17 -7
  74. package/lib/components/Search/variables.js +5 -1
  75. package/lib/components/Segmented/Segmented.js +17 -7
  76. package/lib/components/Select/Select.js +17 -7
  77. package/lib/components/Select/SelectInput.js +17 -7
  78. package/lib/components/Sidebar/Sidebar.js +17 -7
  79. package/lib/components/SidebarActions/styled.js +17 -7
  80. package/lib/components/SkipContent/SkipContent.js +17 -7
  81. package/lib/components/Switch/Switch.js +17 -7
  82. package/lib/components/TableOfContent/TableOfContent.js +17 -7
  83. package/lib/components/Tooltip/Tooltip.js +17 -7
  84. package/lib/components/VersionPicker/VersionPicker.js +17 -7
  85. package/lib/core/constants/search.d.ts +5 -4
  86. package/lib/core/constants/search.js +4 -5
  87. package/lib/core/contexts/CodeSnippetContext.js +17 -7
  88. package/lib/core/hooks/use-tabs.d.ts +3 -2
  89. package/lib/core/hooks/use-tabs.js +115 -57
  90. package/lib/core/templates/Markdown.js +17 -7
  91. package/lib/core/types/hooks.d.ts +6 -3
  92. package/lib/core/types/l10n.d.ts +1 -1
  93. package/lib/core/utils/download-code-walkthrough.js +17 -7
  94. package/lib/core/utils/get-file-icon.js +17 -7
  95. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  96. package/lib/icons/GenericIcon/GenericIcon.js +17 -7
  97. package/lib/icons/RedoclyIcon/RedoclyIcon.js +4 -7
  98. package/lib/icons/Spinner/Spinner.js +17 -7
  99. package/lib/index.js +17 -7
  100. package/lib/layouts/OIDCForbidden.js +17 -7
  101. package/lib/layouts/ThreePanelLayout.js +17 -7
  102. package/lib/markdoc/components/Cards/Cards.js +17 -7
  103. package/lib/markdoc/components/CodeGroup/CodeGroup.js +17 -7
  104. package/lib/markdoc/components/CodeWalkthrough/CodeContainer.js +17 -7
  105. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +17 -7
  106. package/lib/markdoc/components/CodeWalkthrough/CodePanelHeader.js +17 -7
  107. package/lib/markdoc/components/CodeWalkthrough/CodePanelPreview.js +17 -7
  108. package/lib/markdoc/components/CodeWalkthrough/CodePanelToolbar.js +17 -7
  109. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +17 -7
  110. package/lib/markdoc/components/CodeWalkthrough/CodeToggle.js +17 -7
  111. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +17 -7
  112. package/lib/markdoc/components/CodeWalkthrough/Input.js +17 -7
  113. package/lib/markdoc/components/Heading/Heading.js +17 -7
  114. package/lib/markdoc/components/HtmlBlock/HtmlBlock.js +17 -7
  115. package/lib/markdoc/components/InlineSvg/InlineSvg.js +17 -7
  116. package/lib/markdoc/components/MarkdocExample/MarkdocExample.js +17 -7
  117. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  118. package/lib/markdoc/components/Tabs/TabList.js +214 -54
  119. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  120. package/lib/markdoc/components/Tabs/Tabs.js +74 -19
  121. package/lib/markdoc/default.d.ts +104 -1
  122. package/lib/markdoc/default.js +17 -7
  123. package/package.json +5 -5
  124. package/src/components/Catalog/Catalog.tsx +15 -4
  125. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  126. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  127. package/src/components/Dropdown/Dropdown.tsx +84 -79
  128. package/src/components/Menu/MenuItem.tsx +1 -0
  129. package/src/components/Search/SearchAiConversationInput.tsx +12 -2
  130. package/src/components/Search/SearchDialog.tsx +6 -3
  131. package/src/components/Search/variables.ts +5 -1
  132. package/src/core/constants/search.ts +8 -4
  133. package/src/core/hooks/use-tabs.ts +168 -86
  134. package/src/core/types/hooks.ts +6 -1
  135. package/src/core/types/l10n.ts +1 -0
  136. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  137. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +4 -22
  138. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  139. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
@@ -48,6 +48,7 @@ export function SearchDialog({
48
48
  const products = useProducts();
49
49
  const currentProduct = useCurrentProduct();
50
50
  const [product, setProduct] = useState(currentProduct);
51
+ const searchSessionId = `search-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
51
52
  const [mode, setMode] = useState<'search' | 'ai-dialog'>(initialMode);
52
53
  const autoSearchDisabled = mode !== 'search';
53
54
  const {
@@ -62,7 +63,7 @@ export function SearchDialog({
62
63
  advancedSearch,
63
64
  askAi,
64
65
  groupField,
65
- } = useSearch(product?.name, autoSearchDisabled);
66
+ } = useSearch(product?.name, autoSearchDisabled, searchSessionId);
66
67
  const {
67
68
  isFilterOpen,
68
69
  onFilterToggle,
@@ -72,7 +73,7 @@ export function SearchDialog({
72
73
  onQuickFilterReset,
73
74
  } = useSearchFilter(filter, setFilter);
74
75
  const { addSearchHistoryItem } = useRecentSearches();
75
- const aiSearch = useAiSearch({ filter });
76
+ const aiSearch = useAiSearch({ filter }, searchSessionId);
76
77
 
77
78
  const searchInputRef = useRef<HTMLInputElement>(null);
78
79
  const modalRef = useRef<HTMLDivElement>(null);
@@ -150,13 +151,14 @@ export function SearchDialog({
150
151
  totalResults: results.length.toString(),
151
152
  index: index.toString(),
152
153
  searchEngine: mode,
154
+ searchSessionId,
153
155
  });
154
156
  onClose();
155
157
  }}
156
158
  />
157
159
  );
158
160
  },
159
- [onClose, product, products, addSearchHistoryItem, query, telemetry, mode],
161
+ [onClose, product, products, addSearchHistoryItem, query, telemetry, mode, searchSessionId],
160
162
  );
161
163
 
162
164
  const showLoadMore = useCallback(
@@ -389,6 +391,7 @@ export function SearchDialog({
389
391
  telemetry.sendSearchRecentClickedMessage({
390
392
  query,
391
393
  index: index.toString(),
394
+ searchSessionId,
392
395
  });
393
396
  setQuery(query);
394
397
  focusSearchInput();
@@ -171,6 +171,7 @@ export const search = css`
171
171
  --search-ai-assistant-bg-color: var(--layer-color);
172
172
  --search-ai-assistant-text-color: var(--text-color-primary);
173
173
  --search-ai-assistant-border: 1px solid var(--border-color-primary);
174
+ --search-ai-assistant-message-max-width: 80%;
174
175
 
175
176
  --search-ai-resources-gap: var(--spacing-base);
176
177
  --search-ai-resources-title-font-weight: var(--font-weight-medium);
@@ -232,6 +233,7 @@ export const search = css`
232
233
  * @tokens AI Search Conversation Input
233
234
  */
234
235
  --search-ai-conversation-input-bg-color: var(--bg-color);
236
+ --search-ai-conversation-input-bg-color-disabled: var(--color-warm-grey-1);
235
237
  --search-ai-conversation-input-padding: var(--spacing-sm) var(--spacing-md);
236
238
  --search-ai-conversation-input-border: 1px solid var(--border-color-secondary);
237
239
  --search-ai-conversation-input-border-radius: var(--border-radius-lg);
@@ -241,10 +243,12 @@ export const search = css`
241
243
  --search-ai-conversation-input-border-color-disabled: var(--border-color-secondary);
242
244
 
243
245
  --search-ai-conversation-input-send-button-right: 12px;
246
+ --search-ai-conversation-input-send-button-padding: 5px;
244
247
  --search-ai-conversation-input-send-button-bg-color: var(--button-bg-color-primary);
245
248
  --search-ai-conversation-input-send-button-bg-color-hover: var(--button-bg-color-primary-hover);
246
249
  --search-ai-conversation-input-send-button-bg-color-disabled: var(--button-bg-color-disabled);
247
250
  --search-ai-conversation-input-send-button-border-disabled: 1px solid var(--button-border-color-disabled);
251
+ --search-ai-conversation-input-send-button-border-radius: var(--border-radius);
248
252
 
249
253
  /**
250
254
  * @tokens AI Search Response
@@ -277,7 +281,7 @@ export const search = css`
277
281
 
278
282
  --search-ai-suggestions-gap: var(--spacing-sm);
279
283
  --search-ai-suggestions-margin-left: var(--spacing-xs);
280
- --search-ai-suggestion-item-gap: var(--spacing-xs);
284
+ --search-ai-suggestion-item-gap: var(--spacing-xxs);
281
285
 
282
286
  --search-ai-suggestions-title-text-color: var(--text-color-description);
283
287
  --search-ai-suggestions-title-font-size: var(--font-size-base);
@@ -7,10 +7,14 @@ export enum AiSearchError {
7
7
  EmptyResponse = 'empty_response',
8
8
  ErrorProcessingResponse = 'error_processing_response',
9
9
  }
10
- export const enum AiSearchConversationRole {
11
- USER = 'user',
12
- ASSISTANT = 'assistant',
13
- }
10
+
11
+ export const AiSearchConversationRole = {
12
+ USER: 'user',
13
+ ASSISTANT: 'assistant',
14
+ } as const;
15
+
16
+ export type AiSearchConversationRole =
17
+ (typeof AiSearchConversationRole)[keyof typeof AiSearchConversationRole];
14
18
 
15
19
  const defaultErrorConfig: AiSearchErrorConfig = {
16
20
  headerKey: 'search.ai.error.header',
@@ -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 = {
@@ -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;
@@ -142,6 +146,7 @@ export type ThemeHooks = {
142
146
  | undefined;
143
147
  useCatalog: (
144
148
  config?: CatalogEntityConfig,
149
+ serverFilters?: Record<string, { value: string; count: number }[]>,
145
150
  entitiesCounterInitial?: number,
146
151
  initialViewMode?: CatalogViewMode,
147
152
  ) => UseCatalogResponse;
@@ -146,6 +146,7 @@ export type TranslationKey =
146
146
  | 'catalog.entity.metadata.title'
147
147
  | 'catalog.entity.schema.title'
148
148
  | 'catalog.entity.properties.apiDescription.title'
149
+ | 'catalog.backToAllLabel'
149
150
  | 'sidebar.menu.backLabel'
150
151
  | 'sidebar.menu.backToLabel'
151
152
  | 'sidebar.actions.show'
@@ -35,19 +35,28 @@ export const AiStarsIcon = styled(Icon).attrs(() => ({
35
35
  height: ${({ size }) => size || '16px'};
36
36
  width: ${({ size }) => size || '16px'};
37
37
 
38
- ${({ background, borderRadius, margin }) =>
38
+ ${({ background, borderRadius, margin, padding, size }) =>
39
39
  background &&
40
40
  `
41
41
  display: flex;
42
42
  align-items: center;
43
43
  justify-content: center;
44
+ flex-shrink: 0;
44
45
 
45
46
  background: ${getCssColorVariable(background)};
47
+ width: ${size || '16px'};
48
+ height: ${size || '16px'};
46
49
 
47
- padding: var(--spacing-xs);
50
+ padding: ${padding || 'var(--spacing-xs)'};
48
51
  margin: ${margin || '0'};
49
52
 
50
53
  border-radius: ${background && borderRadius ? borderRadius : 'none'};
54
+
55
+ svg {
56
+ width: calc(${size || '16px'} - 2 * (${padding || 'var(--spacing-xs)'}));
57
+ height: calc(${size || '16px'} - 2 * (${padding || 'var(--spacing-xs)'}));
58
+ flex-shrink: 0;
59
+ }
51
60
  `}
52
61
 
53
62
  color: ${({ color }) => color && getCssColorVariable(color)};
@@ -6,29 +6,11 @@ import type { IconProps } from '@redocly/theme/icons/types';
6
6
  import { getCssColorVariable } from '@redocly/theme/core/utils';
7
7
 
8
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
- />
9
+ <svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" {...props}>
10
+ <g>
11
+ <path 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" />
12
+ <path 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" />
26
13
  </g>
27
- <defs>
28
- <clipPath id="clip0_4053_1165">
29
- <rect width="18" height="18" fill="white" />
30
- </clipPath>
31
- </defs>
32
14
  </svg>
33
15
  );
34
16