@redocly/theme 0.70.0-next.3 → 0.70.0-next.5

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 (123) hide show
  1. package/lib/components/Catalog/Catalog.js +1 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityGraph/CatalogEntityRelationsGraph.lazy.d.ts +1 -1
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +2 -2
  4. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +1 -1
  5. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +30 -11
  6. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.js +1 -1
  7. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.js +2 -2
  8. package/lib/components/Dropdown/Dropdown.js +3 -1
  9. package/lib/components/Dropdown/DropdownMenu.d.ts +1 -0
  10. package/lib/components/Dropdown/DropdownMenu.js +1 -1
  11. package/lib/components/Feedback/Reasons.js +3 -2
  12. package/lib/components/Filter/FilterCheckboxes.js +30 -11
  13. package/lib/components/Image/Image.js +24 -4
  14. package/lib/components/Markdown/Markdown.js +3 -2
  15. package/lib/components/Menu/Menu.js +4 -0
  16. package/lib/components/Menu/MenuSelect.d.ts +7 -0
  17. package/lib/components/Menu/MenuSelect.js +129 -0
  18. package/lib/components/Menu/variables.js +43 -4
  19. package/lib/components/Navbar/Navbar.js +44 -5
  20. package/lib/components/Navbar/variables.js +3 -0
  21. package/lib/components/NavigationTabs/NavigationTab.d.ts +7 -0
  22. package/lib/components/NavigationTabs/NavigationTab.js +93 -0
  23. package/lib/components/NavigationTabs/NavigationTabContent.d.ts +7 -0
  24. package/lib/components/NavigationTabs/NavigationTabContent.js +50 -0
  25. package/lib/components/NavigationTabs/NavigationTabMenu.d.ts +7 -0
  26. package/lib/components/NavigationTabs/NavigationTabMenu.js +18 -0
  27. package/lib/components/NavigationTabs/NavigationTabs.d.ts +7 -0
  28. package/lib/components/NavigationTabs/NavigationTabs.js +97 -0
  29. package/lib/components/NavigationTabs/variables.d.ts +1 -0
  30. package/lib/components/NavigationTabs/variables.js +37 -0
  31. package/lib/components/Search/SearchTrigger.js +8 -2
  32. package/lib/components/Segmented/Segmented.js +23 -3
  33. package/lib/components/Select/Select.js +10 -3
  34. package/lib/components/Select/SelectInput.d.ts +1 -0
  35. package/lib/components/Select/SelectInput.js +3 -3
  36. package/lib/components/Sidebar/Sidebar.js +2 -2
  37. package/lib/components/TableOfContent/TableOfContent.js +2 -2
  38. package/lib/core/constants/common.d.ts +1 -0
  39. package/lib/core/constants/common.js +1 -0
  40. package/lib/core/hooks/index.d.ts +2 -0
  41. package/lib/core/hooks/index.js +2 -0
  42. package/lib/core/hooks/menu/use-mobile-menu-items.js +75 -51
  43. package/lib/core/hooks/menu/use-mobile-menu-levels.js +12 -7
  44. package/lib/core/hooks/use-scroll-item-into-view.d.ts +2 -0
  45. package/lib/core/hooks/use-scroll-item-into-view.js +16 -0
  46. package/lib/core/hooks/use-tab-list.d.ts +31 -0
  47. package/lib/core/hooks/use-tab-list.js +49 -0
  48. package/lib/core/hooks/use-theme-hooks.js +2 -0
  49. package/lib/core/styles/global.js +12 -2
  50. package/lib/core/types/hooks.d.ts +5 -0
  51. package/lib/core/types/index.d.ts +1 -0
  52. package/lib/core/types/l10n.d.ts +1 -1
  53. package/lib/core/types/navigation-tabs.d.ts +49 -0
  54. package/lib/core/types/navigation-tabs.js +3 -0
  55. package/lib/core/utils/Dynamic.d.ts +8 -1
  56. package/lib/core/utils/Dynamic.js +34 -3
  57. package/lib/core/utils/index.d.ts +1 -0
  58. package/lib/core/utils/index.js +1 -0
  59. package/lib/core/utils/menu.d.ts +4 -0
  60. package/lib/core/utils/menu.js +42 -3
  61. package/lib/core/utils/navigation-tabs.d.ts +8 -0
  62. package/lib/core/utils/navigation-tabs.js +71 -0
  63. package/lib/index.d.ts +5 -0
  64. package/lib/index.js +7 -1
  65. package/lib/layouts/PageLayout.js +1 -1
  66. package/lib/layouts/RootLayout.js +2 -2
  67. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +1 -1
  68. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +3 -3
  69. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +1 -1
  70. package/package.json +3 -3
  71. package/src/components/Catalog/Catalog.tsx +1 -1
  72. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +2 -2
  73. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +1 -1
  74. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +37 -22
  75. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.tsx +1 -1
  76. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.tsx +2 -2
  77. package/src/components/Dropdown/Dropdown.tsx +4 -1
  78. package/src/components/Dropdown/DropdownMenu.tsx +7 -1
  79. package/src/components/Feedback/Reasons.tsx +3 -2
  80. package/src/components/Filter/FilterCheckboxes.tsx +33 -18
  81. package/src/components/Image/Image.tsx +26 -5
  82. package/src/components/Markdown/Markdown.tsx +3 -2
  83. package/src/components/Menu/Menu.tsx +5 -0
  84. package/src/components/Menu/MenuSelect.tsx +198 -0
  85. package/src/components/Menu/variables.ts +43 -4
  86. package/src/components/Navbar/Navbar.tsx +22 -5
  87. package/src/components/Navbar/variables.ts +3 -0
  88. package/src/components/NavigationTabs/NavigationTab.tsx +148 -0
  89. package/src/components/NavigationTabs/NavigationTabContent.tsx +66 -0
  90. package/src/components/NavigationTabs/NavigationTabMenu.tsx +40 -0
  91. package/src/components/NavigationTabs/NavigationTabs.tsx +83 -0
  92. package/src/components/NavigationTabs/variables.ts +34 -0
  93. package/src/components/Search/SearchTrigger.tsx +11 -2
  94. package/src/components/Segmented/Segmented.tsx +25 -16
  95. package/src/components/Select/Select.tsx +12 -6
  96. package/src/components/Select/SelectInput.tsx +8 -10
  97. package/src/components/Sidebar/Sidebar.tsx +2 -2
  98. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  99. package/src/core/constants/common.ts +1 -0
  100. package/src/core/hooks/__mocks__/use-theme-hooks.ts +3 -0
  101. package/src/core/hooks/index.ts +2 -0
  102. package/src/core/hooks/menu/use-mobile-menu-items.ts +91 -56
  103. package/src/core/hooks/menu/use-mobile-menu-levels.ts +14 -6
  104. package/src/core/hooks/use-scroll-item-into-view.ts +18 -0
  105. package/src/core/hooks/use-tab-list.ts +63 -0
  106. package/src/core/hooks/use-theme-hooks.ts +2 -0
  107. package/src/core/styles/global.ts +12 -2
  108. package/src/core/types/hooks.ts +5 -0
  109. package/src/core/types/index.ts +1 -0
  110. package/src/core/types/l10n.ts +3 -0
  111. package/src/core/types/navigation-tabs.ts +54 -0
  112. package/src/core/utils/Dynamic.tsx +67 -35
  113. package/src/core/utils/index.ts +1 -0
  114. package/src/core/utils/menu.ts +49 -3
  115. package/src/core/utils/navigation-tabs.ts +99 -0
  116. package/src/icons/CheckboxIcon/CheckboxIcon.tsx +1 -0
  117. package/src/icons/ChevronSortIcon/ChevronSortIcon.tsx +1 -0
  118. package/src/index.ts +6 -0
  119. package/src/layouts/PageLayout.tsx +1 -1
  120. package/src/layouts/RootLayout.tsx +2 -2
  121. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +1 -1
  122. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +3 -3
  123. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +1 -1
@@ -1,21 +1,36 @@
1
+ import { useMemo } from 'react';
1
2
  import { useLocation } from 'react-router';
2
3
 
3
- import type { ResolvedNavItem } from '@redocly/config';
4
+ import type { ResolvedNavItem, Version } from '@redocly/config';
4
5
 
5
6
  import { useThemeHooks } from '../use-theme-hooks';
6
7
  import { useThemeConfig } from '../use-theme-config';
7
8
  import { MobileMenuType } from '../../constants/common';
8
- import { buildLanguagesGroup, buildVersionSection, mapNavbarItems } from '../../utils/menu';
9
+ import {
10
+ buildLanguagesGroup,
11
+ buildNavigationTabsSelect,
12
+ buildVersionSection,
13
+ mapNavbarItems,
14
+ } from '../../utils/menu';
9
15
 
10
16
  const MENU_SEPARATOR: ResolvedNavItem = {
11
17
  type: 'separator',
12
18
  separatorLine: true,
13
19
  };
14
20
 
21
+ const NO_VERSIONS: Version[] = [];
22
+ const NO_NAV_ITEMS: ResolvedNavItem[] = [];
23
+
15
24
  export const useMobileMenuItems = (menuType: MobileMenuType) => {
16
- const { useTranslate, useL10nConfig, useCurrentProduct, usePageVersions, useSidebarItems } =
17
- useThemeHooks();
18
- const { versions = [] } = usePageVersions() || {};
25
+ const {
26
+ useTranslate,
27
+ useL10nConfig,
28
+ useCurrentProduct,
29
+ usePageVersions,
30
+ useSidebarItems,
31
+ useNavigationTabs,
32
+ } = useThemeHooks();
33
+ const versions = usePageVersions()?.versions ?? NO_VERSIONS;
19
34
  const { defaultLocale, currentLocale, locales } = useL10nConfig();
20
35
  const { translate } = useTranslate();
21
36
  const activeVersion = versions.find((version) => version.active);
@@ -25,6 +40,7 @@ export const useMobileMenuItems = (menuType: MobileMenuType) => {
25
40
  popDrilldownState,
26
41
  backLink,
27
42
  } = useSidebarItems(activeVersion);
43
+ const navigationTabs = useNavigationTabs();
28
44
  const location = useLocation();
29
45
 
30
46
  const currentProduct = useCurrentProduct();
@@ -33,74 +49,93 @@ export const useMobileMenuItems = (menuType: MobileMenuType) => {
33
49
  const productThemeConfig = useThemeConfig();
34
50
  const baseThemeConfig = useThemeConfig(false);
35
51
 
36
- const baseNavItems = (baseThemeConfig.navbar?.items || []) as ResolvedNavItem[];
52
+ const baseNavItems = (baseThemeConfig.navbar?.items ?? NO_NAV_ITEMS) as ResolvedNavItem[];
37
53
  const productNavItems = (
38
- hasProductNavbarOverride ? productThemeConfig.navbar?.items || [] : []
54
+ hasProductNavbarOverride ? (productThemeConfig.navbar?.items ?? NO_NAV_ITEMS) : NO_NAV_ITEMS
39
55
  ) as ResolvedNavItem[];
40
-
41
- const menuItems: ResolvedNavItem[] = [];
42
-
43
- if (menuType === MobileMenuType.PAGE) {
44
- if (activeVersion) {
45
- // eslint-disable-next-line no-warning-comments
46
- // TODO: consider to refactor
47
- const versionsSection = buildVersionSection(translate, versions, activeVersion);
48
- menuItems.push(...versionsSection);
49
- }
50
- if (menuItems.length && sidebarItems.length) {
51
- menuItems.push(MENU_SEPARATOR);
52
- }
53
- menuItems.push(...(sidebarItems as ResolvedNavItem[]));
54
- } else if (menuType === MobileMenuType.PRODUCT) {
55
- menuItems.push(...productNavItems);
56
-
57
- if (menuItems.length && baseNavItems.length) {
58
- menuItems.push(MENU_SEPARATOR);
59
- }
60
-
61
- if (baseNavItems.length) {
62
- menuItems.push(...baseNavItems);
63
- }
64
- } else if (menuType === MobileMenuType.MAIN_MENU) {
65
- const productMenuItems: ResolvedNavItem[] = Object.values(baseThemeConfig.products || {}).map(
66
- (product) => ({
56
+ const products = baseThemeConfig.products;
57
+
58
+ const items = useMemo(() => {
59
+ const menuItems: ResolvedNavItem[] = [];
60
+ const navigationTabsSelect =
61
+ menuType === MobileMenuType.PAGE ? buildNavigationTabsSelect({ navigationTabs }) : undefined;
62
+
63
+ if (menuType === MobileMenuType.PAGE) {
64
+ if (activeVersion) {
65
+ menuItems.push(...buildVersionSection(translate, versions, activeVersion));
66
+ }
67
+ if (menuItems.length && sidebarItems.length) {
68
+ menuItems.push(MENU_SEPARATOR);
69
+ }
70
+ menuItems.push(...(sidebarItems as ResolvedNavItem[]));
71
+ } else if (menuType === MobileMenuType.PRODUCT) {
72
+ menuItems.push(...productNavItems);
73
+
74
+ if (menuItems.length && baseNavItems.length) {
75
+ menuItems.push(MENU_SEPARATOR);
76
+ }
77
+
78
+ if (baseNavItems.length) {
79
+ menuItems.push(...baseNavItems);
80
+ }
81
+ } else if (menuType === MobileMenuType.MAIN_MENU) {
82
+ const productMenuItems: ResolvedNavItem[] = Object.values(products || {}).map((product) => ({
67
83
  label: product.name,
68
84
  link: product.link,
69
85
  icon: product.icon,
70
86
  type: 'link',
71
- }),
72
- );
87
+ }));
88
+
89
+ if (productMenuItems.length) {
90
+ menuItems.push({
91
+ type: 'separator',
92
+ label: translate('mobileMenu.products', 'Products'),
93
+ });
73
94
 
74
- if (productMenuItems.length) {
75
- menuItems.push({
76
- type: 'separator',
77
- label: translate('mobileMenu.products', 'Products'),
78
- });
95
+ menuItems.push(...productMenuItems);
96
+ }
79
97
 
80
- menuItems.push(...productMenuItems);
98
+ if (menuItems.length && baseNavItems.length) {
99
+ menuItems.push(MENU_SEPARATOR);
100
+ }
101
+
102
+ if (baseNavItems.length) {
103
+ menuItems.push(...baseNavItems);
104
+ }
81
105
  }
82
106
 
83
- if (menuItems.length && baseNavItems.length) {
107
+ const languagesMenu = buildLanguagesGroup(locales, defaultLocale, currentLocale);
108
+
109
+ if (menuItems.length && languagesMenu) {
84
110
  menuItems.push(MENU_SEPARATOR);
85
111
  }
86
112
 
87
- if (baseNavItems.length) {
88
- menuItems.push(...baseNavItems);
113
+ if (languagesMenu) {
114
+ menuItems.push(languagesMenu);
89
115
  }
90
- }
91
-
92
- const languagesMenu = buildLanguagesGroup(locales, defaultLocale, currentLocale);
93
-
94
- if (menuItems.length && languagesMenu) {
95
- menuItems.push(MENU_SEPARATOR);
96
- }
97
116
 
98
- if (languagesMenu) {
99
- menuItems.push(languagesMenu);
100
- }
117
+ return [
118
+ ...(navigationTabsSelect ? [navigationTabsSelect] : []),
119
+ ...mapNavbarItems(menuItems, defaultLocale, currentLocale, locales, location),
120
+ ];
121
+ }, [
122
+ menuType,
123
+ navigationTabs,
124
+ activeVersion,
125
+ versions,
126
+ sidebarItems,
127
+ productNavItems,
128
+ baseNavItems,
129
+ products,
130
+ translate,
131
+ locales,
132
+ defaultLocale,
133
+ currentLocale,
134
+ location,
135
+ ]);
101
136
 
102
137
  return {
103
- items: mapNavbarItems(menuItems, defaultLocale, currentLocale, locales, location),
138
+ items,
104
139
  popDrilldownState,
105
140
  pushDrilldownState,
106
141
  backLink,
@@ -8,27 +8,35 @@ import { useThemeConfig } from '../use-theme-config';
8
8
  import { MobileMenuType } from '../../constants/common';
9
9
 
10
10
  export const useMobileMenuLevels = () => {
11
- const { useTranslate, useCurrentProduct, usePageVersions, useSidebarItems } = useThemeHooks();
11
+ const { useTranslate, useCurrentProduct, usePageVersions, useSidebarItems, useNavigationTabs } =
12
+ useThemeHooks();
12
13
  const { versions = [] } = usePageVersions() || {};
13
14
  const activeVersion = versions.find((version) => version?.active);
14
15
  const { currentItems: sidebarItems } = useSidebarItems(activeVersion);
16
+ const navigationTabs = useNavigationTabs();
15
17
  const { translate } = useTranslate();
16
18
  const currentProduct = useCurrentProduct();
17
19
  const productThemeConfig = useThemeConfig();
20
+ const baseThemeConfig = useThemeConfig(false);
18
21
  const location = useLocation();
19
22
 
20
23
  const hasProductNavbarOverride = currentProduct?.configOverride?.navbar;
21
24
  const productNavItems = (
22
25
  hasProductNavbarOverride ? productThemeConfig.navbar?.items || [] : []
23
26
  ) as ResolvedNavItem[];
27
+ const hasMainMenuItems =
28
+ Object.values(baseThemeConfig.products || {}).length > 0 ||
29
+ (baseThemeConfig.navbar?.items || []).length > 0;
24
30
 
25
31
  const menuLevels = useMemo(() => {
26
- const menuLevels = [
27
- {
32
+ const menuLevels: Array<{ label: string; type: MobileMenuType }> = [];
33
+
34
+ if (hasMainMenuItems) {
35
+ menuLevels.push({
28
36
  label: translate('mobileMenu.mainMenu', 'Main menu'),
29
37
  type: MobileMenuType.MAIN_MENU,
30
- },
31
- ];
38
+ });
39
+ }
32
40
 
33
41
  if (currentProduct && productNavItems.length) {
34
42
  menuLevels.push({
@@ -37,7 +45,7 @@ export const useMobileMenuLevels = () => {
37
45
  });
38
46
  }
39
47
 
40
- if (sidebarItems.length || activeVersion) {
48
+ if (sidebarItems.length || activeVersion || navigationTabs?.tabs.length) {
41
49
  menuLevels.push({
42
50
  label: translate('mobileMenu.previous', 'Previous'),
43
51
  type: MobileMenuType.PAGE,
@@ -0,0 +1,18 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import type { RefObject } from 'react';
4
+
5
+ export function useScrollItemIntoView(
6
+ listRef: RefObject<HTMLElement | null>,
7
+ activeIndex: number,
8
+ ): void {
9
+ useEffect(() => {
10
+ const list = listRef.current;
11
+ const activeItem = activeIndex === -1 ? undefined : list?.children[activeIndex];
12
+ if (!list || !activeItem) return;
13
+
14
+ const listRect = list.getBoundingClientRect();
15
+ const itemRect = activeItem.getBoundingClientRect();
16
+ list.scrollLeft += itemRect.left - listRect.left - (listRect.width - itemRect.width) / 2;
17
+ }, [listRef, activeIndex]);
18
+ }
@@ -0,0 +1,63 @@
1
+ import { useCallback, useId, useRef } from 'react';
2
+
3
+ import type { KeyboardEvent } from 'react';
4
+
5
+ type UseTabListProps = {
6
+ count: number;
7
+ activeIndex: number;
8
+ onChange: (index: number) => void;
9
+ };
10
+
11
+ /**
12
+ * Props for a WAI-ARIA tab list with automatic activation: only the active tab is a tab stop,
13
+ * and Left/Right/Home/End select and focus another tab.
14
+ */
15
+ export function useTabList({ count, activeIndex, onChange }: UseTabListProps) {
16
+ const idPrefix = useId();
17
+ const focusableIndex = activeIndex >= 0 ? activeIndex : 0;
18
+ const tabRefs = useRef<(HTMLElement | null)[]>([]);
19
+
20
+ const select = useCallback(
21
+ (index: number) => {
22
+ onChange(index);
23
+ tabRefs.current[index]?.focus();
24
+ },
25
+ [onChange],
26
+ );
27
+
28
+ const onKeyDown = useCallback(
29
+ (event: KeyboardEvent, index: number) => {
30
+ const target = (<Record<string, number>>{
31
+ ArrowRight: (index + 1) % count,
32
+ ArrowLeft: (index - 1 + count) % count,
33
+ Home: 0,
34
+ End: count - 1,
35
+ })[event.key];
36
+ if (target === undefined) return;
37
+ event.preventDefault();
38
+ select(target);
39
+ },
40
+ [count, select],
41
+ );
42
+
43
+ const getTabProps = (index: number) => ({
44
+ id: `${idPrefix}-tab-${index}`,
45
+ role: 'tab',
46
+ 'aria-selected': index === activeIndex,
47
+ 'aria-controls': `${idPrefix}-panel-${index}`,
48
+ tabIndex: index === focusableIndex ? 0 : -1,
49
+ ref: (element: HTMLElement | null) => {
50
+ tabRefs.current[index] = element;
51
+ },
52
+ onClick: () => select(index),
53
+ onKeyDown: (event: KeyboardEvent) => onKeyDown(event, index),
54
+ });
55
+
56
+ const getTabPanelProps = (index: number) => ({
57
+ id: `${idPrefix}-panel-${index}`,
58
+ role: 'tabpanel',
59
+ 'aria-labelledby': `${idPrefix}-tab-${index}`,
60
+ });
61
+
62
+ return { tabListProps: { role: 'tablist' }, getTabProps, getTabPanelProps };
63
+ }
@@ -39,6 +39,8 @@ const fallbacks = {
39
39
  usePageProps: () => ({}),
40
40
  usePageSharedData: () => null,
41
41
  useMcpData: () => ({ docs: { enabled: false } }),
42
+ useSearchFeatures: () => ({}),
43
+ useNavigationTabs: () => undefined,
42
44
  };
43
45
 
44
46
  export const useThemeHooks = () => {
@@ -49,6 +49,8 @@ import { pageActions } from '@redocly/theme/components/PageActions/variables';
49
49
  import { svgViewer } from '@redocly/theme/components/SvgViewer/variables';
50
50
  import { toast } from '@redocly/theme/components/Toast/variables';
51
51
  import { pageNavigation } from '@redocly/theme/components/PageNavigation/variables';
52
+ import { navigationTabs } from '@redocly/theme/components/NavigationTabs/variables';
53
+ import { breakpoints } from '@redocly/theme/core/utils';
52
54
 
53
55
  import { activeBrandPaletteLight } from './palette';
54
56
  import { darkMode } from './dark';
@@ -1125,8 +1127,8 @@ const error = css`
1125
1127
  --detailed-error-overlay-z-index: var(--z-index-raised);
1126
1128
 
1127
1129
  --detailed-error-modal-width: calc(100% - var(--spacing-lg) * 2);
1128
- --detailed-error-modal-height: calc(100vh - var(--navbar-height) - var(--spacing-lg) * 2);
1129
- --detailed-error-modal-top: calc(var(--spacing-lg) + var(--navbar-height));
1130
+ --detailed-error-modal-height: calc(100vh - var(--navbar-stack-height) - var(--spacing-lg) * 2);
1131
+ --detailed-error-modal-top: calc(var(--spacing-lg) + var(--navbar-stack-height));
1130
1132
  --detailed-error-modal-border-radius: var(--border-radius-md);
1131
1133
  --detailed-error-modal-bg-color: var(--bg-color-raised);
1132
1134
  --detailed-error-modal-color: var(--text-color-primary);
@@ -1373,6 +1375,7 @@ export const styles = css<{ palette?: string }>`
1373
1375
  ${productPicker}
1374
1376
  ${responsePanelColors}
1375
1377
  ${search}
1378
+ ${navigationTabs}
1376
1379
  ${select}
1377
1380
  ${sidebar}
1378
1381
  ${sizeAndSpace}
@@ -1413,6 +1416,13 @@ export const styles = css<{ palette?: string }>`
1413
1416
  ${darkMode};
1414
1417
  }
1415
1418
 
1419
+ @media screen and (min-width: ${breakpoints.medium}) {
1420
+ :root:has([data-component-name='NavigationTabs/NavigationTabs']) {
1421
+ --navbar-stack-height: calc(var(--navbar-height) + var(--navigation-tabs-height, 48px));
1422
+ --navbar-row-bottom-border-color: transparent;
1423
+ }
1424
+ }
1425
+
1416
1426
  :root.notransition * {
1417
1427
  transition: none !important;
1418
1428
  }
@@ -28,6 +28,7 @@ import type {
28
28
  } from './catalog';
29
29
  import type { UserMenuData } from './user-menu';
30
30
  import type { ItemState, DrilldownMenuItemDetails } from './sidebar';
31
+ import type { NavigationTabsState } from './navigation-tabs';
31
32
  import type {
32
33
  SearchItemData,
33
34
  SearchFacet,
@@ -119,6 +120,9 @@ export type ThemeHooks = {
119
120
  askAi?: boolean;
120
121
  retrySearch: () => Promise<void>;
121
122
  };
123
+ useSearchFeatures: () => {
124
+ askAi?: boolean;
125
+ };
122
126
  useAiSearch: (options?: { filter?: SearchFilterItem[]; product?: string }) => {
123
127
  askQuestion: (question: string, history?: AiSearchConversationItem[]) => void;
124
128
  isGeneratingResponse: boolean;
@@ -156,6 +160,7 @@ export type ThemeHooks = {
156
160
  pushDrilldownState: (drilldownDetails: DrilldownMenuItemDetails) => void;
157
161
  backLink?: { slug?: string; label?: string };
158
162
  };
163
+ useNavigationTabs: () => NavigationTabsState | undefined;
159
164
  useSidebarSiblingsData: () =>
160
165
  | {
161
166
  prevPage?: ResolvedNavItemWithLink;
@@ -7,6 +7,7 @@ export type * from './feedback';
7
7
  export type * from './hooks';
8
8
  export type * from './markdown';
9
9
  export type * from './navbar';
10
+ export type * from './navigation-tabs';
10
11
  export * from './search';
11
12
  export type * from './select';
12
13
  export type * from './sidebar';
@@ -222,6 +222,7 @@ export type TranslationKey =
222
222
  | 'catalog.history.revision.current'
223
223
  | 'catalog.history.revisions.showLess'
224
224
  | 'catalog.history.revisions.showMore'
225
+ | 'navigationTabs.label'
225
226
  | 'sidebar.menu.backLabel'
226
227
  | 'sidebar.menu.backToLabel'
227
228
  | 'sidebar.actions.show'
@@ -271,6 +272,7 @@ export type TranslationKey =
271
272
  | 'mobileMenu.previous'
272
273
  | 'mobileMenu.products'
273
274
  | 'mobileMenu.version'
275
+ | 'navbar.label'
274
276
  | 'navbar.products'
275
277
  | 'page.nextButton'
276
278
  | 'page.previousButton'
@@ -360,6 +362,7 @@ export type TranslationKey =
360
362
  | 'openapi.hideExample'
361
363
  | 'openapi.showExample'
362
364
  | 'openapi.expandAll'
365
+ | 'openapi.deepLink.tooltip'
363
366
  | 'openapi.collapseAll'
364
367
  | 'openapi.viewSecurityDetails'
365
368
  | 'openapi.noResponseExample'
@@ -0,0 +1,54 @@
1
+ export type NavigationTabMenuEntry = {
2
+ tabId: string;
3
+ label: string;
4
+ labelTranslationKey?: string;
5
+ link: string;
6
+ icon?: string;
7
+ srcSet?: string;
8
+ external?: boolean;
9
+ target?: '_self' | '_blank';
10
+ };
11
+
12
+ export type NavigationTab = {
13
+ tabId: string;
14
+ label: string;
15
+ labelTranslationKey?: string;
16
+ link?: string;
17
+ icon?: string;
18
+ srcSet?: string;
19
+ menu?: NavigationTabMenuEntry[];
20
+ };
21
+
22
+ export type NavigationTabsState = {
23
+ tabs: NavigationTab[];
24
+ activeTab?: NavigationTab;
25
+ activeMenuEntry?: NavigationTabMenuEntry;
26
+ };
27
+
28
+ export type NavigationTabItem = {
29
+ type: 'link';
30
+ label: string;
31
+ labelTranslationKey?: string;
32
+ link: string;
33
+ icon?: string;
34
+ srcSet?: string;
35
+ external?: boolean;
36
+ target?: '_self' | '_blank';
37
+ active?: boolean;
38
+ };
39
+
40
+ type NavigationTabGroupItem = {
41
+ type: 'group';
42
+ label: string;
43
+ labelTranslationKey?: string;
44
+ icon?: string;
45
+ srcSet?: string;
46
+ items: NavigationTabItem[];
47
+ active?: boolean;
48
+ };
49
+
50
+ export type NavigationTabRowItem = NavigationTabItem | NavigationTabGroupItem;
51
+
52
+ export type NavigationTabsConfig = {
53
+ items: NavigationTabRowItem[];
54
+ };
@@ -5,60 +5,86 @@ interface DynamicOptions<TProps extends Record<string, unknown>> {
5
5
  loading?: React.ComponentType<TProps>;
6
6
  }
7
7
 
8
+ /**
9
+ * A component from `dynamic()`. After `preload()` resolves, the component renders without a
10
+ * loading state. On the server, `preload()` does not load a client-only component (`ssr: false`).
11
+ */
12
+ export type DynamicComponent<TProps> = React.ComponentType<TProps> & {
13
+ preload(): Promise<void>;
14
+ };
15
+
8
16
  export function dynamic<TProps extends Record<string, unknown>>(
9
17
  factory: () => Promise<
10
18
  React.ComponentType<TProps> | { default: React.ComponentType<TProps> } | Record<string, unknown>
11
19
  >,
12
20
  { ssr = true, loading }: DynamicOptions<TProps> = {},
13
- ): React.ComponentType<TProps> {
14
- const Component = React.lazy(() =>
15
- factory().then((mod) => {
16
- // Case 1: factory() => import('./SomeModule') - direct import
17
- // Case 2: factory() => import('./SomeModule').then(mod => mod.default) - extract default
18
- if (typeof mod === 'function') {
19
- return { default: mod };
20
- }
21
-
22
- // Case 3: factory() => import('./SomeModule').then(mod => ({ default: mod.default })) - manual wrap
23
- // Case 4: factory() => import('./SomeModule').then(mod => ({ default: mod.Named })) - manual wrap named
24
- if (mod && typeof mod === 'object' && 'default' in mod) {
25
- const defaultExport = mod.default;
26
-
27
- // Handle both function components and React component objects (e.g., memo/forwardRef)
28
- if (typeof defaultExport === 'function') {
29
- return { default: defaultExport as React.ComponentType<TProps> };
21
+ ): DynamicComponent<TProps> {
22
+ let loaded: React.ComponentType<TProps> | undefined;
23
+ let pending: Promise<{ default: React.ComponentType<TProps> }> | undefined;
24
+
25
+ const load = () =>
26
+ (pending ??= factory()
27
+ .then((mod) => {
28
+ // Case 1: factory() => import('./SomeModule') - direct import
29
+ // Case 2: factory() => import('./SomeModule').then(mod => mod.default) - extract default
30
+ if (typeof mod === 'function') {
31
+ return { default: mod };
30
32
  }
31
33
 
32
- if (defaultExport && typeof defaultExport === 'object' && '$$typeof' in defaultExport) {
33
- return { default: defaultExport as React.ComponentType<TProps> };
34
+ // Case 3: factory() => import('./SomeModule').then(mod => ({ default: mod.default })) - manual wrap
35
+ // Case 4: factory() => import('./SomeModule').then(mod => ({ default: mod.Named })) - manual wrap named
36
+ if (mod && typeof mod === 'object' && 'default' in mod) {
37
+ const defaultExport = mod.default;
38
+
39
+ // Handle both function components and React component objects (e.g., memo/forwardRef)
40
+ if (typeof defaultExport === 'function') {
41
+ return { default: defaultExport as React.ComponentType<TProps> };
42
+ }
43
+
44
+ if (defaultExport && typeof defaultExport === 'object' && '$$typeof' in defaultExport) {
45
+ return { default: defaultExport as React.ComponentType<TProps> };
46
+ }
34
47
  }
35
- }
36
48
 
37
- // Case 5: factory() => import('./SomeModule').then(mod => mod.Named) - find named export
38
- if (mod && typeof mod === 'object') {
39
- const modObj = mod as Record<string, unknown>;
49
+ // Case 5: factory() => import('./SomeModule').then(mod => mod.Named) - find named export
50
+ if (mod && typeof mod === 'object') {
51
+ const modObj = mod as Record<string, unknown>;
40
52
 
41
- for (const key in modObj) {
42
- const value = modObj[key];
53
+ for (const key in modObj) {
54
+ const value = modObj[key];
43
55
 
44
- if (typeof value === 'function') {
45
- return { default: value as React.ComponentType<TProps> };
46
- }
56
+ if (typeof value === 'function') {
57
+ return { default: value as React.ComponentType<TProps> };
58
+ }
47
59
 
48
- if (value && typeof value === 'object' && '$$typeof' in value) {
49
- return { default: value as React.ComponentType<TProps> };
60
+ if (value && typeof value === 'object' && '$$typeof' in value) {
61
+ return { default: value as React.ComponentType<TProps> };
62
+ }
50
63
  }
51
64
  }
52
- }
53
65
 
54
- throw new Error('Dynamic import did not return a valid React component');
55
- }),
56
- );
66
+ throw new Error('Dynamic import did not return a valid React component');
67
+ })
68
+ .then(
69
+ (module) => {
70
+ loaded = module.default;
71
+ return module;
72
+ },
73
+ (error: unknown) => {
74
+ // A failed load is not kept, so the next preload tries again.
75
+ pending = undefined;
76
+ throw error;
77
+ },
78
+ ));
79
+
80
+ const LazyComponent = React.lazy(load);
57
81
 
58
82
  const LoadingComponent: React.ComponentType<TProps> = loading || (() => null);
59
83
 
60
- return (props: TProps) => {
84
+ const DynamicComponent = (props: TProps) => {
61
85
  const [hasMounted, setHasMounted] = React.useState(false);
86
+ // Fixed per instance: a change of the element type would remount the component.
87
+ const [Component] = React.useState(() => loaded ?? LazyComponent);
62
88
 
63
89
  React.useEffect(() => {
64
90
  setHasMounted(true);
@@ -82,4 +108,10 @@ export function dynamic<TProps extends Record<string, unknown>>(
82
108
  </React.Suspense>
83
109
  );
84
110
  };
111
+
112
+ return Object.assign(DynamicComponent, {
113
+ async preload() {
114
+ if (ssr || typeof window !== 'undefined') await load();
115
+ },
116
+ });
85
117
  }
@@ -17,6 +17,7 @@ export * from './type-guards';
17
17
  export * from './dom';
18
18
  export * from './toc';
19
19
  export * from './menu';
20
+ export * from './navigation-tabs';
20
21
  export * from './load-and-navigate';
21
22
  export * from './get-scorecard-color-variable';
22
23
  export * from './change-text-casing';