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

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 (96) hide show
  1. package/lib/components/Catalog/Catalog.js +1 -1
  2. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +2 -2
  3. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +1 -1
  4. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.js +1 -1
  5. package/lib/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.js +2 -2
  6. package/lib/components/Dropdown/Dropdown.js +3 -1
  7. package/lib/components/Markdown/Markdown.js +3 -2
  8. package/lib/components/Menu/Menu.js +4 -0
  9. package/lib/components/Menu/MenuSelect.d.ts +7 -0
  10. package/lib/components/Menu/MenuSelect.js +129 -0
  11. package/lib/components/Menu/variables.js +43 -4
  12. package/lib/components/Navbar/Navbar.js +44 -5
  13. package/lib/components/Navbar/variables.js +3 -0
  14. package/lib/components/NavigationTabs/NavigationTab.d.ts +7 -0
  15. package/lib/components/NavigationTabs/NavigationTab.js +93 -0
  16. package/lib/components/NavigationTabs/NavigationTabContent.d.ts +7 -0
  17. package/lib/components/NavigationTabs/NavigationTabContent.js +50 -0
  18. package/lib/components/NavigationTabs/NavigationTabMenu.d.ts +7 -0
  19. package/lib/components/NavigationTabs/NavigationTabMenu.js +18 -0
  20. package/lib/components/NavigationTabs/NavigationTabs.d.ts +7 -0
  21. package/lib/components/NavigationTabs/NavigationTabs.js +97 -0
  22. package/lib/components/NavigationTabs/variables.d.ts +1 -0
  23. package/lib/components/NavigationTabs/variables.js +37 -0
  24. package/lib/components/Search/SearchTrigger.js +8 -2
  25. package/lib/components/Sidebar/Sidebar.js +2 -2
  26. package/lib/components/TableOfContent/TableOfContent.js +2 -2
  27. package/lib/core/constants/common.d.ts +1 -0
  28. package/lib/core/constants/common.js +1 -0
  29. package/lib/core/hooks/index.d.ts +1 -0
  30. package/lib/core/hooks/index.js +1 -0
  31. package/lib/core/hooks/menu/use-mobile-menu-items.js +75 -51
  32. package/lib/core/hooks/menu/use-mobile-menu-levels.js +12 -7
  33. package/lib/core/hooks/use-scroll-item-into-view.d.ts +2 -0
  34. package/lib/core/hooks/use-scroll-item-into-view.js +16 -0
  35. package/lib/core/hooks/use-theme-hooks.js +2 -0
  36. package/lib/core/styles/global.js +12 -2
  37. package/lib/core/types/hooks.d.ts +5 -0
  38. package/lib/core/types/index.d.ts +1 -0
  39. package/lib/core/types/l10n.d.ts +1 -1
  40. package/lib/core/types/navigation-tabs.d.ts +49 -0
  41. package/lib/core/types/navigation-tabs.js +3 -0
  42. package/lib/core/utils/index.d.ts +1 -0
  43. package/lib/core/utils/index.js +1 -0
  44. package/lib/core/utils/menu.d.ts +4 -0
  45. package/lib/core/utils/menu.js +42 -3
  46. package/lib/core/utils/navigation-tabs.d.ts +8 -0
  47. package/lib/core/utils/navigation-tabs.js +71 -0
  48. package/lib/index.d.ts +5 -0
  49. package/lib/index.js +7 -1
  50. package/lib/layouts/PageLayout.js +1 -1
  51. package/lib/layouts/RootLayout.js +2 -2
  52. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +1 -1
  53. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +3 -3
  54. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +1 -1
  55. package/package.json +3 -3
  56. package/src/components/Catalog/Catalog.tsx +1 -1
  57. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +2 -2
  58. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +1 -1
  59. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.tsx +1 -1
  60. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.tsx +2 -2
  61. package/src/components/Dropdown/Dropdown.tsx +4 -1
  62. package/src/components/Markdown/Markdown.tsx +3 -2
  63. package/src/components/Menu/Menu.tsx +5 -0
  64. package/src/components/Menu/MenuSelect.tsx +198 -0
  65. package/src/components/Menu/variables.ts +43 -4
  66. package/src/components/Navbar/Navbar.tsx +22 -5
  67. package/src/components/Navbar/variables.ts +3 -0
  68. package/src/components/NavigationTabs/NavigationTab.tsx +148 -0
  69. package/src/components/NavigationTabs/NavigationTabContent.tsx +66 -0
  70. package/src/components/NavigationTabs/NavigationTabMenu.tsx +40 -0
  71. package/src/components/NavigationTabs/NavigationTabs.tsx +83 -0
  72. package/src/components/NavigationTabs/variables.ts +34 -0
  73. package/src/components/Search/SearchTrigger.tsx +11 -2
  74. package/src/components/Sidebar/Sidebar.tsx +2 -2
  75. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  76. package/src/core/constants/common.ts +1 -0
  77. package/src/core/hooks/__mocks__/use-theme-hooks.ts +2 -0
  78. package/src/core/hooks/index.ts +1 -0
  79. package/src/core/hooks/menu/use-mobile-menu-items.ts +91 -56
  80. package/src/core/hooks/menu/use-mobile-menu-levels.ts +14 -6
  81. package/src/core/hooks/use-scroll-item-into-view.ts +18 -0
  82. package/src/core/hooks/use-theme-hooks.ts +2 -0
  83. package/src/core/styles/global.ts +12 -2
  84. package/src/core/types/hooks.ts +5 -0
  85. package/src/core/types/index.ts +1 -0
  86. package/src/core/types/l10n.ts +3 -0
  87. package/src/core/types/navigation-tabs.ts +54 -0
  88. package/src/core/utils/index.ts +1 -0
  89. package/src/core/utils/menu.ts +49 -3
  90. package/src/core/utils/navigation-tabs.ts +99 -0
  91. package/src/index.ts +6 -0
  92. package/src/layouts/PageLayout.tsx +1 -1
  93. package/src/layouts/RootLayout.tsx +2 -2
  94. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +1 -1
  95. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +3 -3
  96. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +1 -1
@@ -0,0 +1,83 @@
1
+ import React, { useRef } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { NavigationTabsConfig } from '@redocly/theme/core/types';
6
+
7
+ import { breakpoints } from '@redocly/theme/core/utils';
8
+ import { useScrollItemIntoView, useThemeHooks } from '@redocly/theme/core/hooks';
9
+ import { NavigationTab } from '@redocly/theme/components/NavigationTabs/NavigationTab';
10
+
11
+ export type NavigationTabsProps = {
12
+ config?: NavigationTabsConfig;
13
+ className?: string;
14
+ };
15
+
16
+ export function NavigationTabs({ config, className }: NavigationTabsProps): JSX.Element | null {
17
+ const { useTranslate } = useThemeHooks();
18
+ const { translate } = useTranslate();
19
+ const listRef = useRef<HTMLUListElement | null>(null);
20
+
21
+ const tabs = config?.items ?? [];
22
+ const activeIndex = tabs.findIndex((tab) => tab.active);
23
+
24
+ useScrollItemIntoView(listRef, activeIndex);
25
+
26
+ if (!tabs.length) {
27
+ return null;
28
+ }
29
+
30
+ return (
31
+ <NavigationTabsWrapper
32
+ data-component-name="NavigationTabs/NavigationTabs"
33
+ className={className}
34
+ aria-label={translate('navigationTabs.label', 'Navigation tabs')}
35
+ >
36
+ <NavigationTabsList ref={listRef}>
37
+ {tabs.map((tab, index) => (
38
+ <NavigationTab key={`${tab.label}_${index}`} tab={tab} />
39
+ ))}
40
+ </NavigationTabsList>
41
+ </NavigationTabsWrapper>
42
+ );
43
+ }
44
+
45
+ const NavigationTabsWrapper = styled.nav`
46
+ display: none;
47
+ box-sizing: border-box;
48
+ width: 100%;
49
+ height: var(--navigation-tabs-height);
50
+ max-width: var(--navbar-container-max-width);
51
+ padding: var(--navigation-tabs-padding);
52
+ background: var(--navigation-tabs-bg-color);
53
+ border-bottom: var(--navigation-tabs-bottom-border);
54
+ flex-shrink: 0;
55
+
56
+ @media screen and (min-width: ${breakpoints.medium}) {
57
+ display: flex;
58
+ }
59
+
60
+ @media screen and (min-width: ${breakpoints.max}) {
61
+ max-width: var(--container-max-width);
62
+ margin-left: auto;
63
+ margin-right: auto;
64
+ }
65
+ `;
66
+
67
+ const NavigationTabsList = styled.ul`
68
+ list-style: none;
69
+ margin: 0 0 calc(-1 * var(--navigation-tabs-border-width));
70
+ padding: 0;
71
+ display: flex;
72
+ flex-wrap: nowrap;
73
+ align-items: stretch;
74
+ gap: var(--navigation-tabs-gap);
75
+ min-width: 0;
76
+ overflow-x: auto;
77
+ scrollbar-width: none;
78
+ -ms-overflow-style: none;
79
+
80
+ &::-webkit-scrollbar {
81
+ display: none;
82
+ }
83
+ `;
@@ -0,0 +1,34 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const navigationTabs = css`
4
+ --navigation-tabs-height: 48px; // @presenter Spacing
5
+ --navigation-tabs-bg-color: transparent; // @presenter Color
6
+ --navigation-tabs-border-color: var(--border-color-secondary);
7
+ --navigation-tabs-border-width: 1px;
8
+ --navigation-tabs-bottom-border: var(--navigation-tabs-border-width) solid var(--navigation-tabs-border-color);
9
+ --navigation-tabs-padding-vertical: 0;
10
+ --navigation-tabs-padding-horizontal: var(--spacing-xl);
11
+ --navigation-tabs-padding: var(--navigation-tabs-padding-vertical) var(--navigation-tabs-padding-horizontal); // @presenter Spacing
12
+ --navigation-tabs-gap: var(--spacing-base); // @presenter Spacing
13
+
14
+ --navigation-tabs-item-padding-vertical: var(--spacing-xxs);
15
+ --navigation-tabs-item-padding-horizontal: 0;
16
+ --navigation-tabs-item-padding: var(--navigation-tabs-item-padding-vertical) var(--navigation-tabs-item-padding-horizontal);
17
+ --navigation-tabs-item-content-gap: var(--spacing-xs);
18
+ --navigation-tabs-item-font-size: var(--font-size-base);
19
+ --navigation-tabs-item-line-height: var(--line-height-base);
20
+ --navigation-tabs-item-font-weight: var(--font-weight-regular);
21
+ --navigation-tabs-item-font-weight-active: var(--font-weight-medium);
22
+ --navigation-tabs-item-text-color: var(--navbar-text-color); // @presenter Color
23
+ --navigation-tabs-item-text-color-hover: var(--text-color-primary); // @presenter Color
24
+ --navigation-tabs-item-text-color-active: var(--text-color-primary); // @presenter Color
25
+ --navigation-tabs-item-bottom-border: var(--navigation-tabs-border-width) solid transparent;
26
+ --navigation-tabs-item-bottom-border-hover: var(--navigation-tabs-border-width) solid var(--border-color-primary);
27
+ --navigation-tabs-item-bottom-border-active: var(--navigation-tabs-border-width) solid var(--border-color-inverse);
28
+ --navigation-tabs-item-border-color-focused: var(--button-border-color-focused);
29
+ --navigation-tabs-item-border-radius: var(--navbar-item-border-radius);
30
+ --navigation-tabs-item-icon-size: 1.25em; // @presenter Spacing
31
+ --navigation-tabs-item-external-icon-margin-left: 0;
32
+ --navigation-tabs-item-label-font-weight-active: var(--navigation-tabs-item-font-weight-active);
33
+ --navigation-tabs-item-transition: 150ms ease-in-out;
34
+ `;
@@ -31,7 +31,10 @@ export function SearchTrigger({ onClick, className }: SearchTriggerProps): JSX.E
31
31
  }
32
32
 
33
33
  mainShortcutKey = mainShortcutKey?.toUpperCase();
34
- const isMac = getUserAgent().includes('Mac');
34
+ // A page can be cached and served to every platform, so the server renders a hidden
35
+ // placeholder and the browser picks the label after hydration.
36
+ const [isMac, setIsMac] = React.useState<boolean | undefined>(undefined);
37
+ React.useEffect(() => setIsMac(getUserAgent().includes('Mac')), []);
35
38
 
36
39
  return (
37
40
  <SearchTriggerWrapper
@@ -53,7 +56,9 @@ export function SearchTrigger({ onClick, className }: SearchTriggerProps): JSX.E
53
56
  <SearchIcon />
54
57
  {translate('search.navbar.label', 'Search')}
55
58
  {mainShortcutKey === DEFAULT_KEYBOARD_SHORTCUTS ? (
56
- <span>{isMac ? '⌘K' : 'Ctrl+K'}</span>
59
+ <SearchTriggerShortcut $isPending={isMac === undefined}>
60
+ {isMac ? '⌘K' : 'Ctrl+K'}
61
+ </SearchTriggerShortcut>
57
62
  ) : (
58
63
  <span>{mainShortcutKey}</span>
59
64
  )}
@@ -80,6 +85,10 @@ const SearchTriggerButton = styled(Button)`
80
85
  }
81
86
  `;
82
87
 
88
+ const SearchTriggerShortcut = styled.span<{ $isPending: boolean }>`
89
+ visibility: ${({ $isPending }) => ($isPending ? 'hidden' : 'visible')};
90
+ `;
91
+
83
92
  const SearchTriggerInput = styled.div`
84
93
  display: none;
85
94
  align-items: center;
@@ -84,8 +84,8 @@ const SidebarContent = styled.aside<{ $opened: boolean; $collapsed?: boolean }>`
84
84
  flex-direction: column;
85
85
  width: 100%;
86
86
  -webkit-font-smoothing: antialiased;
87
- top: calc(var(--navbar-height) + var(--banner-height));
88
- height: calc(100vh - var(--navbar-height) - var(--banner-height));
87
+ top: var(--navbar-offset);
88
+ height: calc(100vh - var(--navbar-offset));
89
89
  overflow-x: hidden;
90
90
  ${({ $opened }) => `
91
91
  display: ${$opened ? 'flex' : 'none'}
@@ -101,8 +101,8 @@ const TableOfContentHeader = styled.div`
101
101
 
102
102
  const TableOfContentItems = styled.div`
103
103
  position: sticky;
104
- top: calc(var(--navbar-height) + var(--toc-offset-top));
105
- max-height: calc(100vh - var(--navbar-height) - var(--toc-offset-top));
104
+ top: calc(var(--navbar-stack-height) + var(--toc-offset-top));
105
+ max-height: calc(100vh - var(--navbar-stack-height) - var(--toc-offset-top));
106
106
  overflow-y: auto;
107
107
  width: var(--toc-width);
108
108
  border-left: solid 1px var(--toc-border-color);
@@ -18,6 +18,7 @@ export enum FEEDBACK_TYPES {
18
18
  export const DEFAULT_FEEDBACK_TYPE = FEEDBACK_TYPES.SENTIMENT;
19
19
 
20
20
  export enum MenuItemType {
21
+ Select = 'select',
21
22
  Separator = 'separator',
22
23
  DrillDown = 'drilldown',
23
24
  Group = 'group',
@@ -70,6 +70,7 @@ export const useThemeHooks = vi.fn(() => ({
70
70
  items: [],
71
71
  isLoading: false,
72
72
  })),
73
+ useSearchFeatures: vi.fn(() => ({ askAi: false })),
73
74
  useAiSearch: vi.fn(() => ({
74
75
  askQuestion: vi.fn(),
75
76
  isGeneratingResponse: false,
@@ -117,6 +118,7 @@ export const useThemeHooks = vi.fn(() => ({
117
118
  pushDrilldownState: vi.fn(),
118
119
  }),
119
120
  useSidebarSiblingsData: vi.fn(),
121
+ useNavigationTabs: vi.fn(),
120
122
  useSubmitFeedback: vi.fn(() => ({
121
123
  submitFeedback: vi.fn(),
122
124
  })),
@@ -58,3 +58,4 @@ export * from './use-toast-logic';
58
58
  export * from './use-banner-telemetry';
59
59
  export * from './use-tooltip-fallback-placement';
60
60
  export * from './use-anchor-positioning';
61
+ export * from './use-scroll-item-into-view';
@@ -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
+ }
@@ -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
+ };
@@ -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';