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

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 (221) hide show
  1. package/LICENSE +7 -1
  2. package/lib/components/Accordion/Accordion.js +17 -7
  3. package/lib/components/Accordion/AccordionBody.js +17 -7
  4. package/lib/components/Admonition/Admonition.js +17 -7
  5. package/lib/components/Badge/Badge.js +17 -7
  6. package/lib/components/Breadcrumbs/Breadcrumb.js +17 -7
  7. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +17 -7
  8. package/lib/components/Button/Button.js +17 -7
  9. package/lib/components/Buttons/AIAssistantButton.js +23 -9
  10. package/lib/components/Buttons/CopyButton.js +17 -7
  11. package/lib/components/Buttons/variables.js +1 -1
  12. package/lib/components/Catalog/Catalog.d.ts +6 -0
  13. package/lib/components/Catalog/Catalog.js +7 -6
  14. package/lib/components/Catalog/CatalogEntities.js +17 -7
  15. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.js +17 -7
  16. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.js +17 -7
  17. package/lib/components/Catalog/CatalogEntity/CatalogEntityInfoBar.js +1 -0
  18. package/lib/components/Catalog/CatalogEntity/CatalogEntityMetadata.js +17 -7
  19. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.js +1 -1
  20. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +17 -7
  21. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.js +1 -1
  22. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +17 -7
  23. package/lib/components/Catalog/CatalogEntityIcon.js +2 -1
  24. package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +4 -0
  25. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +17 -7
  26. package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.js +17 -7
  27. package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +17 -7
  28. package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +17 -7
  29. package/lib/components/Catalog/CatalogSortButton.js +17 -7
  30. package/lib/components/Catalog/CatalogTableView/CatalogTableHeaderCell.js +17 -7
  31. package/lib/components/Catalog/CatalogViewModeToggle.js +17 -7
  32. package/lib/components/Catalog/variables.js +1 -1
  33. package/lib/components/CatalogClassic/CatalogClassicActions.js +17 -7
  34. package/lib/components/CatalogClassic/CatalogClassicCard.js +17 -7
  35. package/lib/components/CatalogClassic/CatalogClassicHighlight.js +17 -7
  36. package/lib/components/CatalogClassic/CatalogClassicVirtualizedGroups.js +17 -7
  37. package/lib/components/CodeBlock/CodeBlock.js +17 -7
  38. package/lib/components/CodeBlock/CodeBlockContainer.js +17 -7
  39. package/lib/components/CodeBlock/CodeBlockTabs.js +17 -7
  40. package/lib/components/Dropdown/Dropdown.d.ts +16 -2
  41. package/lib/components/Dropdown/Dropdown.js +22 -12
  42. package/lib/components/Dropdown/DropdownMenuItem.js +17 -7
  43. package/lib/components/Feedback/Comment.js +17 -7
  44. package/lib/components/Feedback/Feedback.js +17 -7
  45. package/lib/components/Feedback/Mood.js +17 -7
  46. package/lib/components/Feedback/Rating.js +17 -7
  47. package/lib/components/Feedback/Reasons.js +17 -7
  48. package/lib/components/Feedback/Scale.js +17 -7
  49. package/lib/components/Feedback/Sentiment.js +17 -7
  50. package/lib/components/Feedback/Stars.js +17 -7
  51. package/lib/components/Filter/FilterContent.js +17 -7
  52. package/lib/components/Filter/FilterInput.js +17 -7
  53. package/lib/components/Image/Image.js +17 -7
  54. package/lib/components/JsonViewer/JsonViewer.js +17 -7
  55. package/lib/components/JsonViewer/helpers.js +17 -7
  56. package/lib/components/LastUpdated/LastUpdated.js +17 -7
  57. package/lib/components/Link/Link.js +17 -7
  58. package/lib/components/Markdown/Markdown.js +17 -7
  59. package/lib/components/Marker/Marker.js +17 -7
  60. package/lib/components/Menu/MenuContainer.js +17 -7
  61. package/lib/components/Menu/MenuItem.js +18 -8
  62. package/lib/components/Menu/MenuMobile.js +17 -7
  63. package/lib/components/Navbar/NavbarItem.js +3 -3
  64. package/lib/components/PageActions/PageActions.js +17 -7
  65. package/lib/components/PageNavigation/NextButton.js +17 -7
  66. package/lib/components/Panel/Panel.js +17 -7
  67. package/lib/components/Panel/PanelBody.js +17 -7
  68. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +17 -7
  69. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +1 -2
  70. package/lib/components/Search/SearchAiActionButtons.d.ts +10 -0
  71. package/lib/components/Search/SearchAiActionButtons.js +43 -0
  72. package/lib/components/Search/SearchAiConversationInput.d.ts +3 -1
  73. package/lib/components/Search/SearchAiConversationInput.js +56 -14
  74. package/lib/components/Search/SearchAiDialog.d.ts +3 -6
  75. package/lib/components/Search/SearchAiDialog.js +37 -16
  76. package/lib/components/Search/SearchAiMessage.d.ts +9 -5
  77. package/lib/components/Search/SearchAiMessage.js +146 -22
  78. package/lib/components/Search/SearchAiNegativeFeedbackForm.d.ts +8 -0
  79. package/lib/components/Search/SearchAiNegativeFeedbackForm.js +169 -0
  80. package/lib/components/Search/SearchDialog.js +53 -12
  81. package/lib/components/Search/SearchFilter.js +17 -7
  82. package/lib/components/Search/SearchGroups.js +19 -9
  83. package/lib/components/Search/SearchHighlight.js +17 -7
  84. package/lib/components/Search/SearchItem.js +17 -7
  85. package/lib/components/Search/SearchRecent.js +17 -7
  86. package/lib/components/Search/SearchShortcut.js +17 -7
  87. package/lib/components/Search/SearchSuggestedPages.js +17 -7
  88. package/lib/components/Search/SearchTrigger.js +17 -7
  89. package/lib/components/Search/variables.js +36 -64
  90. package/lib/components/Segmented/Segmented.js +17 -7
  91. package/lib/components/Select/Select.js +17 -7
  92. package/lib/components/Select/SelectInput.js +18 -8
  93. package/lib/components/Sidebar/Sidebar.js +17 -7
  94. package/lib/components/SidebarActions/styled.js +17 -7
  95. package/lib/components/SkipContent/SkipContent.js +17 -7
  96. package/lib/components/Switch/Switch.js +17 -7
  97. package/lib/components/TableOfContent/TableOfContent.js +17 -7
  98. package/lib/components/Tag/Tag.d.ts +2 -1
  99. package/lib/components/Tag/Tag.js +67 -18
  100. package/lib/components/Tag/variables.dark.js +135 -36
  101. package/lib/components/Tag/variables.js +78 -61
  102. package/lib/components/Tooltip/Tooltip.js +17 -7
  103. package/lib/components/VersionPicker/VersionPicker.js +17 -7
  104. package/lib/core/constants/search.d.ts +5 -4
  105. package/lib/core/constants/search.js +4 -5
  106. package/lib/core/contexts/CodeSnippetContext.js +17 -7
  107. package/lib/core/hooks/index.d.ts +1 -0
  108. package/lib/core/hooks/index.js +1 -0
  109. package/lib/core/hooks/menu/use-nested-menu.js +1 -1
  110. package/lib/core/hooks/search/use-feedback-tooltip.d.ts +6 -0
  111. package/lib/core/hooks/search/use-feedback-tooltip.js +26 -0
  112. package/lib/core/hooks/use-product-picker.js +2 -1
  113. package/lib/core/hooks/use-tabs.d.ts +3 -2
  114. package/lib/core/hooks/use-tabs.js +115 -57
  115. package/lib/core/hooks/use-telemetry-fallback.d.ts +10 -8
  116. package/lib/core/hooks/use-telemetry-fallback.js +10 -8
  117. package/lib/core/styles/dark.js +4 -0
  118. package/lib/core/styles/global.js +4 -0
  119. package/lib/core/templates/Markdown.js +17 -7
  120. package/lib/core/types/hooks.d.ts +6 -3
  121. package/lib/core/types/l10n.d.ts +1 -1
  122. package/lib/core/types/search.d.ts +11 -4
  123. package/lib/core/types/search.js +6 -0
  124. package/lib/core/utils/download-code-walkthrough.js +17 -7
  125. package/lib/core/utils/frontmatter-translate.d.ts +6 -0
  126. package/lib/core/utils/frontmatter-translate.js +14 -0
  127. package/lib/core/utils/get-file-icon.js +17 -7
  128. package/lib/core/utils/index.d.ts +1 -0
  129. package/lib/core/utils/index.js +1 -0
  130. package/lib/icons/AiStarsGradientIcon/AiStarsGradientIcon.js +44 -4
  131. package/lib/icons/AiStarsIcon/AiStarsIcon.js +11 -2
  132. package/lib/icons/CubeIcon/CubeIcon.d.ts +9 -0
  133. package/lib/icons/CubeIcon/CubeIcon.js +17 -0
  134. package/lib/icons/GenericIcon/GenericIcon.js +17 -7
  135. package/lib/icons/HashtagIcon/HashtagIcon.d.ts +9 -0
  136. package/lib/icons/HashtagIcon/HashtagIcon.js +22 -0
  137. package/lib/icons/RedoclyIcon/RedoclyIcon.js +4 -7
  138. package/lib/icons/Spinner/Spinner.js +17 -7
  139. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.d.ts +9 -0
  140. package/lib/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.js +34 -0
  141. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.d.ts +9 -0
  142. package/lib/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.js +34 -0
  143. package/lib/index.d.ts +1 -0
  144. package/lib/index.js +18 -7
  145. package/lib/layouts/OIDCForbidden.js +17 -7
  146. package/lib/layouts/ThreePanelLayout.js +17 -7
  147. package/lib/markdoc/components/Cards/Card.js +1 -28
  148. package/lib/markdoc/components/Cards/Cards.js +17 -7
  149. package/lib/markdoc/components/CodeGroup/CodeGroup.js +17 -7
  150. package/lib/markdoc/components/CodeWalkthrough/CodeContainer.js +17 -7
  151. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +17 -7
  152. package/lib/markdoc/components/CodeWalkthrough/CodePanelHeader.js +17 -7
  153. package/lib/markdoc/components/CodeWalkthrough/CodePanelPreview.js +17 -7
  154. package/lib/markdoc/components/CodeWalkthrough/CodePanelToolbar.js +17 -7
  155. package/lib/markdoc/components/CodeWalkthrough/CodeStep.js +17 -7
  156. package/lib/markdoc/components/CodeWalkthrough/CodeToggle.js +17 -7
  157. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +17 -7
  158. package/lib/markdoc/components/CodeWalkthrough/Input.js +17 -7
  159. package/lib/markdoc/components/Heading/Heading.js +17 -7
  160. package/lib/markdoc/components/HtmlBlock/HtmlBlock.js +17 -7
  161. package/lib/markdoc/components/InlineSvg/InlineSvg.js +17 -7
  162. package/lib/markdoc/components/MarkdocExample/MarkdocExample.js +17 -7
  163. package/lib/markdoc/components/Tabs/TabList.d.ts +3 -1
  164. package/lib/markdoc/components/Tabs/TabList.js +214 -54
  165. package/lib/markdoc/components/Tabs/Tabs.d.ts +2 -1
  166. package/lib/markdoc/components/Tabs/Tabs.js +74 -19
  167. package/lib/markdoc/default.d.ts +104 -1
  168. package/lib/markdoc/default.js +17 -7
  169. package/lib/markdoc/tags/card.js +0 -1
  170. package/package.json +8 -8
  171. package/src/components/Buttons/AIAssistantButton.tsx +6 -2
  172. package/src/components/Buttons/variables.ts +1 -1
  173. package/src/components/Catalog/Catalog.tsx +15 -4
  174. package/src/components/Catalog/CatalogEntity/CatalogEntityInfoBar.tsx +1 -0
  175. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityApiDescriptionRelations.tsx +1 -1
  176. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityTeamRelations.tsx +1 -1
  177. package/src/components/Catalog/CatalogEntityIcon.tsx +2 -1
  178. package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +5 -0
  179. package/src/components/Catalog/variables.ts +1 -1
  180. package/src/components/Dropdown/Dropdown.tsx +84 -79
  181. package/src/components/Menu/MenuItem.tsx +1 -0
  182. package/src/components/Navbar/NavbarItem.tsx +6 -5
  183. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +3 -3
  184. package/src/components/Search/SearchAiActionButtons.tsx +76 -0
  185. package/src/components/Search/SearchAiConversationInput.tsx +61 -18
  186. package/src/components/Search/SearchAiDialog.tsx +52 -23
  187. package/src/components/Search/SearchAiMessage.tsx +172 -43
  188. package/src/components/Search/SearchAiNegativeFeedbackForm.tsx +210 -0
  189. package/src/components/Search/SearchDialog.tsx +49 -13
  190. package/src/components/Search/SearchGroups.tsx +2 -0
  191. package/src/components/Search/variables.ts +36 -64
  192. package/src/components/Select/SelectInput.tsx +1 -0
  193. package/src/components/Tag/Tag.tsx +36 -20
  194. package/src/components/Tag/variables.dark.ts +135 -36
  195. package/src/components/Tag/variables.ts +78 -61
  196. package/src/core/constants/search.ts +8 -4
  197. package/src/core/hooks/index.ts +1 -0
  198. package/src/core/hooks/menu/use-nested-menu.ts +2 -2
  199. package/src/core/hooks/search/use-feedback-tooltip.ts +32 -0
  200. package/src/core/hooks/use-product-picker.ts +2 -1
  201. package/src/core/hooks/use-tabs.ts +168 -86
  202. package/src/core/hooks/use-telemetry-fallback.ts +10 -8
  203. package/src/core/styles/dark.ts +4 -0
  204. package/src/core/styles/global.ts +4 -0
  205. package/src/core/types/hooks.ts +6 -1
  206. package/src/core/types/l10n.ts +5 -0
  207. package/src/core/types/search.ts +13 -4
  208. package/src/core/utils/frontmatter-translate.ts +9 -0
  209. package/src/core/utils/index.ts +1 -0
  210. package/src/icons/AiStarsGradientIcon/AiStarsGradientIcon.tsx +13 -4
  211. package/src/icons/AiStarsIcon/AiStarsIcon.tsx +11 -2
  212. package/src/icons/CubeIcon/CubeIcon.tsx +27 -0
  213. package/src/icons/HashtagIcon/HashtagIcon.tsx +23 -0
  214. package/src/icons/RedoclyIcon/RedoclyIcon.tsx +4 -22
  215. package/src/icons/ThumbDownFilledIcon/ThumbDownFilledIcon.tsx +38 -0
  216. package/src/icons/ThumbUpFilledIcon/ThumbUpFilledIcon.tsx +35 -0
  217. package/src/index.ts +1 -0
  218. package/src/markdoc/components/Cards/Card.tsx +1 -28
  219. package/src/markdoc/components/Tabs/TabList.tsx +312 -105
  220. package/src/markdoc/components/Tabs/Tabs.tsx +136 -11
  221. package/src/markdoc/tags/card.ts +0 -1
@@ -0,0 +1,38 @@
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
+ const { color, ...restProps } = props;
10
+
11
+ return (
12
+ <svg
13
+ width="14"
14
+ height="14"
15
+ viewBox="0 0 14 14"
16
+ fill="none"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ {...restProps}
19
+ >
20
+ <path
21
+ d="M3.0625 0.875H0.875V7H3.0625V0.875Z"
22
+ fill={getCssColorVariable(color) || '#DCDDE5'}
23
+ />
24
+ <path
25
+ d="M10.0625 0.875H3.9375V7.35118L5.26846 9.34771L5.63828 11.9362C5.66886 12.1443 5.77308 12.3346 5.932 12.4725C6.09092 12.6103 6.29402 12.6866 6.5044 12.6875H6.5625C6.91048 12.6871 7.2441 12.5487 7.49016 12.3027C7.73621 12.0566 7.87462 11.723 7.875 11.375V8.75H11.375C11.839 8.74948 12.2838 8.56494 12.6119 8.23686C12.9399 7.90879 13.1245 7.46397 13.125 7V3.9375C13.1241 3.12555 12.8012 2.34712 12.227 1.77298C11.6529 1.19885 10.8744 0.875903 10.0625 0.875Z"
26
+ fill={getCssColorVariable(color) || '#DCDDE5'}
27
+ />
28
+ </svg>
29
+ );
30
+ };
31
+
32
+ export const ThumbDownFilledIcon = styled(Icon).attrs(() => ({
33
+ 'data-component-name': 'icons/ThumbDownFilledIcon/ThumbDownFilledIcon',
34
+ }))<IconProps>`
35
+ height: ${({ size }) => size || '14px'};
36
+ width: ${({ size }) => size || '14px'};
37
+ color: ${({ color }) => color && getCssColorVariable(color)};
38
+ `;
@@ -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
+ import { getCssColorVariable } from '@redocly/theme/core/utils';
7
+
8
+ const Icon = (props: IconProps) => {
9
+ const { color, ...restProps } = props;
10
+
11
+ return (
12
+ <svg
13
+ width="14"
14
+ height="14"
15
+ viewBox="0 0 14 14"
16
+ fill="none"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ {...restProps}
19
+ >
20
+ <path d="M3.0625 7H0.875V13.125H3.0625V7Z" fill={getCssColorVariable(color) || '#DCDDE5'} />
21
+ <path
22
+ d="M10.0625 13.125H3.9375V6.64882L5.26846 4.65229L5.63828 2.06382C5.66886 1.85567 5.77308 1.66539 5.932 1.52753C6.09092 1.38967 6.29402 1.31338 6.5044 1.3125H6.5625C6.91048 1.31288 7.2441 1.45129 7.49016 1.69734C7.73621 1.9434 7.87462 2.27702 7.875 2.625V5.25H11.375C11.839 5.25052 12.2838 5.43506 12.6119 5.76314C12.9399 6.09121 13.1245 6.53603 13.125 7V10.0625C13.1241 10.8744 12.8012 11.6529 12.227 12.227C11.6529 12.8012 10.8744 13.1241 10.0625 13.125Z"
23
+ fill={getCssColorVariable(color) || '#DCDDE5'}
24
+ />
25
+ </svg>
26
+ );
27
+ };
28
+
29
+ export const ThumbUpFilledIcon = styled(Icon).attrs(() => ({
30
+ 'data-component-name': 'icons/ThumbUpFilledIcon/ThumbUpFilledIcon',
31
+ }))<IconProps>`
32
+ height: ${({ size }) => size || '14px'};
33
+ width: ${({ size }) => size || '14px'};
34
+ color: ${({ color }) => color && getCssColorVariable(color)};
35
+ `;
package/src/index.ts CHANGED
@@ -287,6 +287,7 @@ export * from '@redocly/theme/icons/RedoclyIcon/RedoclyIcon';
287
287
  export * from '@redocly/theme/icons/WorkflowHierarchyIcon/WorkflowHierarchyIcon';
288
288
  export * from '@redocly/theme/icons/GenericIcon/GenericIcon';
289
289
  export * from '@redocly/theme/icons/ShareIcon/ShareIcon';
290
+ export * from '@redocly/theme/icons/HashtagIcon/HashtagIcon';
290
291
  /* Layouts */
291
292
  export * from '@redocly/theme/layouts/RootLayout';
292
293
  export * from '@redocly/theme/layouts/PageLayout';
@@ -103,6 +103,7 @@ const ContentWrapper = styled.div`
103
103
  flex-direction: column;
104
104
  flex-shrink: 1;
105
105
  gap: var(--card-content-gap);
106
+ width: 100%;
106
107
  `;
107
108
 
108
109
  const Title = styled.h3<{ $isCardLink: boolean; $justifyContent: string }>`
@@ -146,28 +147,6 @@ const Body = styled.div<{ $lineClamp?: number }>`
146
147
  > *:last-child {
147
148
  margin-bottom: 0;
148
149
  }
149
-
150
- /* Icon link styles */
151
- & a {
152
- display: inline-flex;
153
- align-items: center;
154
- line-height: 1;
155
-
156
- & svg {
157
- width: 1.2em;
158
- height: 1.2em;
159
- margin-right: 0.25em;
160
- vertical-align: middle;
161
-
162
- & g {
163
- fill: var(--link-color-primary);
164
- }
165
- }
166
- }
167
-
168
- & a:visited > span > svg > g {
169
- fill: var(--link-color-visited);
170
- }
171
150
  `;
172
151
 
173
152
  const CardWrapper = styled.div.attrs<{
@@ -222,12 +201,6 @@ const CardWrapper = styled.div.attrs<{
222
201
  }
223
202
  }
224
203
  }
225
-
226
- && ul {
227
- list-style: none;
228
- padding-left: 0;
229
- margin: var(--spacing-xs) 0;
230
- }
231
204
  `;
232
205
 
233
206
  const getCardWrapperClass = ($isCardLink: boolean, $variant?: string) => {
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
@@ -17,6 +17,140 @@ type TabListProps = {
17
17
  size: TabsSize;
18
18
  activeTab: string;
19
19
  onTabChange: (tab: string) => void;
20
+ containerRef: React.RefObject<HTMLUListElement | null>;
21
+ onReadyChange?: (isReady: boolean) => void;
22
+ };
23
+
24
+ type UseHighlightBarAnimationProps = {
25
+ childrenArray: React.ReactElement<TabItemProps>[];
26
+ activeTab: string;
27
+ tabsContainerRef: React.RefObject<HTMLElement | null>;
28
+ visibleTabs: number[];
29
+ overflowTabs: number[];
30
+ };
31
+
32
+ /**
33
+ * Calculates optimal dropdown position relative to viewport to ensure visibility.
34
+ * Positions below the button by default, but moves above if insufficient space.
35
+ * Adjusts horizontal position to prevent overflow off screen edges.
36
+ */
37
+ const calculateDropdownPosition = (
38
+ buttonRect: DOMRect,
39
+ dropdownRect: DOMRect,
40
+ ): { top: number; left: number } => {
41
+ const gap = 4;
42
+ const margin = 16;
43
+ const spaceBelow = window.innerHeight - buttonRect.bottom;
44
+ const spaceAbove = buttonRect.top;
45
+
46
+ // Position below button, or above if dropdown doesn't fit below
47
+ const top =
48
+ spaceBelow < dropdownRect.height + gap && spaceAbove > spaceBelow
49
+ ? buttonRect.top - gap
50
+ : buttonRect.bottom + gap;
51
+
52
+ // Align with button left edge, adjust if overflows screen
53
+ const idealLeft = buttonRect.left;
54
+ const rightEdge = idealLeft + dropdownRect.width;
55
+ const overflowsRight = rightEdge > window.innerWidth - margin;
56
+
57
+ const left = overflowsRight
58
+ ? window.innerWidth - dropdownRect.width - margin
59
+ : Math.max(margin, idealLeft);
60
+
61
+ return { top, left };
62
+ };
63
+
64
+ /**
65
+ * Manages dropdown positioning and updates on scroll/resize events for TabList.
66
+ */
67
+ const useDropdownPosition = (
68
+ hasOverflow: boolean,
69
+ dropdownRef: React.RefObject<HTMLDivElement | null>,
70
+ ) => {
71
+ const [dropdownPosition, setDropdownPosition] = useState<{ top?: number; left?: number }>({});
72
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
73
+
74
+ const updateDropdownPosition = useCallback(() => {
75
+ if (!dropdownRef.current) return;
76
+
77
+ const button = dropdownRef.current.querySelector('button');
78
+ const dropdownMenu = dropdownRef.current.querySelector('div:last-child');
79
+ if (!button || !dropdownMenu) return;
80
+
81
+ const buttonRect = button.getBoundingClientRect();
82
+ const dropdownRect = (dropdownMenu as HTMLElement).getBoundingClientRect();
83
+
84
+ const position = calculateDropdownPosition(buttonRect, dropdownRect);
85
+ setDropdownPosition(position);
86
+ }, [dropdownRef]);
87
+
88
+ // Track when dropdown menu appears and recalculate position
89
+ useEffect(() => {
90
+ if (!hasOverflow || !isDropdownOpen || !dropdownRef.current) return;
91
+
92
+ const dropdownMenu = dropdownRef.current.querySelector('div:last-child') as HTMLElement;
93
+ if (!dropdownMenu) return;
94
+
95
+ // ResizeObserver tracks both initial render and size changes
96
+ const resizeObserver = new ResizeObserver(() => {
97
+ updateDropdownPosition();
98
+ });
99
+
100
+ resizeObserver.observe(dropdownMenu);
101
+
102
+ return () => resizeObserver.disconnect();
103
+ }, [hasOverflow, isDropdownOpen, dropdownRef, updateDropdownPosition]);
104
+
105
+ // Update position on scroll/resize
106
+ useEffect(() => {
107
+ if (!hasOverflow || !isDropdownOpen) return;
108
+
109
+ window.addEventListener('scroll', updateDropdownPosition, true);
110
+ window.addEventListener('resize', updateDropdownPosition);
111
+
112
+ return () => {
113
+ window.removeEventListener('scroll', updateDropdownPosition, true);
114
+ window.removeEventListener('resize', updateDropdownPosition);
115
+ };
116
+ }, [hasOverflow, isDropdownOpen, updateDropdownPosition]);
117
+
118
+ return {
119
+ dropdownPosition,
120
+ isDropdownOpen,
121
+ setIsDropdownOpen,
122
+ setDropdownPosition,
123
+ updateDropdownPosition,
124
+ };
125
+ };
126
+
127
+ const renderTab = (
128
+ child: React.ReactElement<TabItemProps>,
129
+ index: number,
130
+ size: TabsSize,
131
+ setTabRef: (element: HTMLButtonElement | null, index: number) => void,
132
+ handleKeyboard: (event: React.KeyboardEvent, index: number) => void,
133
+ onTabClick: (labelOrIndex: string | number) => void,
134
+ ) => {
135
+ const { label, icon } = child.props;
136
+ const tabId = getTabId(label, index);
137
+
138
+ return (
139
+ <Tab
140
+ key={`key-${tabId}`}
141
+ tabId={tabId}
142
+ label={label}
143
+ icon={icon}
144
+ size={size}
145
+ disabled={child.props.disable}
146
+ setRef={(el: HTMLButtonElement | null) => setTabRef(el, index)}
147
+ onKeyDown={(event) => handleKeyboard(event, index)}
148
+ onClick={() => {
149
+ child.props.onClick?.();
150
+ onTabClick(label);
151
+ }}
152
+ />
153
+ );
20
154
  };
21
155
 
22
156
  export function TabList({
@@ -24,106 +158,119 @@ export function TabList({
24
158
  size,
25
159
  activeTab,
26
160
  onTabChange,
161
+ containerRef,
162
+ onReadyChange,
27
163
  }: TabListProps): JSX.Element {
28
- const tabsContainerRef = useRef<HTMLUListElement>(null);
29
-
30
- const { allTabsHidden, overflowTabs, visibleTabs, handleKeyboard, onTabClick, setTabRef } =
31
- useTabs({
32
- activeTab,
33
- onTabChange,
34
- containerRef: tabsContainerRef,
35
- totalTabs: childrenArray.length,
36
- });
164
+ const dropdownRef = useRef<HTMLDivElement>(null);
165
+ const totalTabs = childrenArray.length;
166
+
167
+ const { overflowTabs, visibleTabs, handleKeyboard, onTabClick, setTabRef, isReady } = useTabs({
168
+ activeTab,
169
+ onTabChange,
170
+ containerRef,
171
+ totalTabs,
172
+ });
173
+
174
+ useEffect(() => {
175
+ onReadyChange?.(isReady);
176
+ }, [isReady, onReadyChange]);
37
177
 
38
178
  const { highlightStyle } = useHighlightBarAnimation({
39
179
  activeTab,
40
180
  childrenArray,
41
181
  overflowTabs,
42
- tabsContainerRef,
182
+ tabsContainerRef: containerRef,
43
183
  visibleTabs,
44
184
  });
45
185
 
186
+ const hasOverflow = overflowTabs.length > 0;
187
+ const isMoreActive =
188
+ hasOverflow &&
189
+ overflowTabs.some((i) => childrenArray[i] && activeTab === childrenArray[i].props.label);
190
+
191
+ // Show as selector when no visible tabs (all tabs in dropdown)
192
+ const showAsSelector = visibleTabs.length === 0 && hasOverflow;
193
+
194
+ const { dropdownPosition, setIsDropdownOpen, setDropdownPosition } = useDropdownPosition(
195
+ hasOverflow,
196
+ dropdownRef,
197
+ );
198
+
46
199
  return (
47
- <TabListContainer role="tablist" ref={tabsContainerRef}>
200
+ <TabListContainer role="tablist" ref={containerRef}>
48
201
  <HighlightBar size={size} style={highlightStyle}>
49
202
  <div />
50
203
  </HighlightBar>
204
+
51
205
  {childrenArray.map((child, index) => {
52
- if (!visibleTabs.includes(index)) return null;
53
- const { label, icon } = child.props;
54
- const tabId = getTabId(label, index);
55
- return (
56
- <Tab
57
- key={`key-${tabId}`}
58
- tabId={tabId}
59
- label={label}
60
- icon={icon}
61
- size={size}
62
- disabled={child.props.disable}
63
- setRef={(el: HTMLButtonElement | null) => setTabRef(el, index)}
64
- onKeyDown={(event) => handleKeyboard(event, index)}
65
- onClick={() => {
66
- child.props.onClick?.();
67
- onTabClick(label);
68
- }}
69
- />
70
- );
206
+ // Show all tabs before ready (for measurement), then only visible ones
207
+ const shouldRender = !isReady || visibleTabs.includes(index);
208
+ if (!shouldRender) return null;
209
+ return renderTab(child, index, size, setTabRef, handleKeyboard, onTabClick);
71
210
  })}
72
- <TabItem
73
- size={size}
74
- active={overflowTabs.some((index) => activeTab === childrenArray[index].props.label)}
75
- tabIndex={0}
76
- >
77
- {overflowTabs.length > 0 && (
78
- <Dropdown
79
- trigger={
80
- <TabButtonLink
81
- size={size}
82
- className={
83
- overflowTabs.some((index) => activeTab === childrenArray[index].props.label)
84
- ? 'active'
85
- : undefined
86
- }
87
- >
88
- {allTabsHidden ? activeTab : 'More'}
89
- </TabButtonLink>
90
- }
91
- alignment="start"
92
- withArrow={true}
211
+
212
+ {hasOverflow && (
213
+ <TabItem
214
+ size={size}
215
+ active={isMoreActive || showAsSelector}
216
+ tabIndex={0}
217
+ className="dropdown-tab"
218
+ >
219
+ <DropdownWrapper
220
+ $top={dropdownPosition.top}
221
+ $left={dropdownPosition.left}
222
+ onClickCapture={() => {
223
+ setIsDropdownOpen(true);
224
+ }}
93
225
  >
94
- <DropdownMenu>
95
- {overflowTabs.map((index) => {
96
- const { label } = childrenArray[index].props;
97
- const tabId = getTabId(label, index);
98
- return (
99
- <DropdownMenuItem
100
- key={`more-${tabId}`}
101
- active={activeTab === label}
102
- onAction={() => {
103
- childrenArray[index].props.onClick?.();
104
- onTabClick(index);
105
- }}
106
- disabled={childrenArray[index].props.disable}
107
- >
108
- {label}
109
- </DropdownMenuItem>
110
- );
111
- })}
112
- </DropdownMenu>
113
- </Dropdown>
114
- )}
115
- </TabItem>
226
+ <FixedPositionDropdown
227
+ ref={dropdownRef}
228
+ trigger={
229
+ <TabButtonLink
230
+ size={size}
231
+ className={isMoreActive || showAsSelector ? 'active' : undefined}
232
+ >
233
+ {showAsSelector ? <TabButtonText>{activeTab}</TabButtonText> : 'More'}
234
+ </TabButtonLink>
235
+ }
236
+ alignment="start"
237
+ withArrow
238
+ onClose={() => {
239
+ setIsDropdownOpen(false);
240
+ setDropdownPosition({});
241
+ }}
242
+ >
243
+ <DropdownMenu>
244
+ {overflowTabs.map((index) => {
245
+ const child = childrenArray[index];
246
+ if (!child) return null;
247
+
248
+ const { label } = child.props;
249
+ const tabId = getTabId(label, index);
250
+
251
+ return (
252
+ <DropdownMenuItem
253
+ key={`more-${tabId}`}
254
+ active={activeTab === label}
255
+ onAction={() => {
256
+ child.props.onClick?.();
257
+ onTabClick(index);
258
+ }}
259
+ disabled={child.props.disable}
260
+ >
261
+ {label}
262
+ </DropdownMenuItem>
263
+ );
264
+ })}
265
+ </DropdownMenu>
266
+ </FixedPositionDropdown>
267
+ </DropdownWrapper>
268
+ </TabItem>
269
+ )}
116
270
  </TabListContainer>
117
271
  );
118
272
  }
119
273
 
120
- type UseHighlightBarAnimationProps = {
121
- childrenArray: React.ReactElement<TabItemProps>[];
122
- activeTab: string;
123
- tabsContainerRef: React.RefObject<HTMLElement | null>;
124
- visibleTabs: number[];
125
- overflowTabs: number[];
126
- };
127
274
  const useHighlightBarAnimation = (props: UseHighlightBarAnimationProps) => {
128
275
  const { childrenArray, activeTab, tabsContainerRef, visibleTabs, overflowTabs } = props;
129
276
 
@@ -141,35 +288,39 @@ const useHighlightBarAnimation = (props: UseHighlightBarAnimationProps) => {
141
288
  return;
142
289
  }
143
290
 
144
- const activeTabElement: HTMLElement | null = container.querySelector(
145
- `[data-label="${activeTab}"]`,
146
- );
147
- if (!activeTabElement) return;
148
-
291
+ // Remove active class from all tabs first
149
292
  container.querySelectorAll('[data-label]').forEach((el) => {
150
293
  el.classList.remove('active');
151
294
  });
152
295
 
153
- const { offsetLeft, offsetWidth } = activeTabElement;
154
-
155
- if (visibleTabs.includes(activeIndex)) {
156
- activeTabElement.classList.add('active');
157
- setHighlightStyle({ left: offsetLeft, width: offsetWidth });
158
- return;
159
- }
160
-
296
+ // Check if active tab is in overflow first
161
297
  if (overflowTabs.includes(activeIndex)) {
162
298
  const moreButton = container.querySelector('button');
163
299
  if (!moreButton) return;
164
300
 
165
301
  const moreButtonRect = moreButton.getBoundingClientRect();
166
302
  const containerRect = container.getBoundingClientRect();
303
+
167
304
  setHighlightStyle({
168
305
  left: moreButtonRect.left - containerRect.left,
169
306
  width: moreButtonRect.width,
170
307
  });
171
308
  return;
172
309
  }
310
+
311
+ // Active tab is visible, find its element
312
+ const activeTabElement: HTMLElement | null = container.querySelector(
313
+ `[data-label="${activeTab}"]`,
314
+ );
315
+ if (!activeTabElement) return;
316
+
317
+ const { offsetLeft, offsetWidth } = activeTabElement;
318
+
319
+ if (visibleTabs.includes(activeIndex)) {
320
+ activeTabElement.classList.add('active');
321
+ setHighlightStyle({ left: offsetLeft, width: offsetWidth });
322
+ return;
323
+ }
173
324
  }, [activeTab, childrenArray, visibleTabs, overflowTabs, tabsContainerRef]);
174
325
 
175
326
  return { highlightStyle };
@@ -181,15 +332,11 @@ export const TabListContainer = styled.ul`
181
332
  gap: var(--md-tabs-gap);
182
333
  width: 100%;
183
334
  min-width: 0;
184
- position: relative;
185
335
 
186
336
  &::before {
187
337
  content: '';
188
338
  position: absolute;
189
- top: 0px;
190
- left: 0px;
191
- right: 0px;
192
- bottom: 0px;
339
+ inset: 0;
193
340
  border: var(--md-tabs-border);
194
341
  border-width: var(--md-tabs-border-width);
195
342
  pointer-events: none;
@@ -197,11 +344,17 @@ export const TabListContainer = styled.ul`
197
344
 
198
345
  && {
199
346
  padding: var(--md-tabs-padding);
200
- margin-block-end: 0;
201
347
  margin: 0;
202
348
 
203
349
  & > li {
204
- margin-bottom: 0px;
350
+ margin-bottom: 0;
351
+ flex-shrink: 0;
352
+
353
+ &.dropdown-tab {
354
+ flex-shrink: 1;
355
+ min-width: 0;
356
+ max-width: 100%;
357
+ }
205
358
  }
206
359
  }
207
360
  `;
@@ -212,7 +365,7 @@ export const TabItem = styled.li<{ active?: boolean; size: TabsSize; tabIndex?:
212
365
  cursor: pointer;
213
366
  align-items: center;
214
367
  padding: var(--md-tabs-tab-wrapper-padding);
215
- z-index: 1;
368
+ z-index: var(--z-index-surface);
216
369
 
217
370
  ${({ active, size }) =>
218
371
  active
@@ -251,6 +404,51 @@ export const TabItem = styled.li<{ active?: boolean; size: TabsSize; tabIndex?:
251
404
  }
252
405
  `;
253
406
 
407
+ const DropdownWrapper = styled.div.attrs<{ $top?: number; $left?: number }>((props) => ({
408
+ style: {
409
+ ...(props.$top !== undefined && { '--dropdown-top': `${props.$top}px` }),
410
+ ...(props.$left !== undefined && { '--dropdown-left': `${props.$left}px` }),
411
+ },
412
+ }))<{ $top?: number; $left?: number }>`
413
+ position: static;
414
+ z-index: var(--z-index-raised);
415
+ width: 100%;
416
+ min-width: 0;
417
+ `;
418
+
419
+ const FixedPositionDropdown = styled(Dropdown)`
420
+ position: static;
421
+ width: 100%;
422
+ min-width: 0;
423
+
424
+ > div:first-child {
425
+ width: 100%;
426
+ min-width: 0;
427
+ }
428
+
429
+ > div:last-child {
430
+ position: fixed;
431
+ top: var(--dropdown-top, 0);
432
+ left: var(--dropdown-left, 0);
433
+ right: auto;
434
+ bottom: auto;
435
+ transform: none;
436
+ padding-top: 0;
437
+ max-width: min(400px, calc(100vw - 32px));
438
+ max-height: calc(100vh - var(--dropdown-top, 0) - 32px);
439
+ overflow-y: auto;
440
+ z-index: var(--z-index-raised);
441
+
442
+ ul {
443
+ li {
444
+ overflow: hidden;
445
+ text-overflow: ellipsis;
446
+ white-space: nowrap;
447
+ }
448
+ }
449
+ }
450
+ `;
451
+
254
452
  const HighlightBar = styled.div<{ size: TabsSize }>`
255
453
  position: absolute;
256
454
  top: 0;
@@ -271,11 +469,20 @@ const HighlightBar = styled.div<{ size: TabsSize }>`
271
469
  }
272
470
  `;
273
471
 
472
+ const TabButtonText = styled.span`
473
+ overflow: hidden;
474
+ text-overflow: ellipsis;
475
+ white-space: nowrap;
476
+ flex: 1;
477
+ min-width: 0;
478
+ `;
479
+
274
480
  export const TabButtonLink = styled(Button)`
275
481
  color: var(--md-tabs-tab-text-color);
276
482
  font-family: var(--md-tabs-tab-font-family);
277
483
  font-style: var(--md-tabs-tab-font-style);
278
484
  background-color: var(--md-tabs-tab-bg-color);
485
+ width: 100%;
279
486
 
280
487
  transition:
281
488
  background-color 300ms ease-in-out,
@@ -295,9 +502,9 @@ export const TabButtonLink = styled(Button)`
295
502
 
296
503
  &.active {
297
504
  color: var(--md-tabs-active-tab-text-color);
298
- font-size: var(--md-tabs-${({ size }) => size}-active-tab-font-size);
299
505
  font-family: var(--md-tabs-active-tab-font-family);
300
506
  font-style: var(--md-tabs-active-tab-font-style);
507
+ font-size: var(--md-tabs-${({ size }) => size}-active-tab-font-size);
301
508
  font-weight: var(--md-tabs-${({ size }) => size}-active-tab-font-weight);
302
509
  line-height: var(--md-tabs-${({ size }) => size}-active-tab-line-height);
303
510
  background-color: var(--md-tabs-active-tab-bg-color);
@@ -307,12 +514,12 @@ export const TabButtonLink = styled(Button)`
307
514
 
308
515
  &:hover {
309
516
  color: var(--md-tabs-hover-tab-text-color);
310
- font-size: var(--md-tabs-${({ size }) => size}-hover-tab-font-size);
311
517
  font-family: var(--md-tabs-hover-tab-font-family);
312
518
  font-style: var(--md-tabs-hover-tab-font-style);
519
+ font-size: var(--md-tabs-${({ size }) => size}-hover-tab-font-size);
313
520
  font-weight: var(--md-tabs-${({ size }) => size}-hover-tab-font-weight);
314
- background-color: var(--md-tabs-hover-tab-bg-color);
315
521
  line-height: var(--md-tabs-${({ size }) => size}-hover-tab-line-height);
522
+ background-color: var(--md-tabs-hover-tab-bg-color);
316
523
  border-radius: var(--md-tabs-${({ size }) => size}-hover-tab-border-radius);
317
524
  padding: var(--md-tabs-${({ size }) => size}-hover-tab-padding);
318
525
  }