@redocly/theme 0.70.0-next.2 → 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 (155) 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/CodeBlock/CodeBlockControls.js +3 -5
  7. package/lib/components/Consent/Consent.d.ts +2 -0
  8. package/lib/components/Consent/Consent.js +17 -0
  9. package/lib/components/Consent/ConsentBanner.d.ts +2 -0
  10. package/lib/components/Consent/ConsentBanner.js +120 -0
  11. package/lib/components/Consent/ConsentDialog.d.ts +2 -0
  12. package/lib/components/Consent/ConsentDialog.js +128 -0
  13. package/lib/components/Consent/ConsentSettingsLink.d.ts +2 -0
  14. package/lib/components/Consent/ConsentSettingsLink.js +35 -0
  15. package/lib/components/Consent/variables.d.ts +1 -0
  16. package/lib/components/Consent/variables.js +31 -0
  17. package/lib/components/Dropdown/Dropdown.js +3 -1
  18. package/lib/components/Footer/Footer.js +4 -1
  19. package/lib/components/JsonViewer/Helpers.d.ts +10 -2
  20. package/lib/components/JsonViewer/Helpers.js +29 -33
  21. package/lib/components/JsonViewer/JsonViewer.js +15 -18
  22. package/lib/components/Markdown/Markdown.js +3 -2
  23. package/lib/components/Markdown/styles/base-table.js +14 -1
  24. package/lib/components/Markdown/variables.js +1 -0
  25. package/lib/components/Menu/Menu.js +4 -0
  26. package/lib/components/Menu/MenuSelect.d.ts +7 -0
  27. package/lib/components/Menu/MenuSelect.js +129 -0
  28. package/lib/components/Menu/variables.js +43 -4
  29. package/lib/components/Navbar/Navbar.js +44 -5
  30. package/lib/components/Navbar/variables.js +3 -0
  31. package/lib/components/NavigationTabs/NavigationTab.d.ts +7 -0
  32. package/lib/components/NavigationTabs/NavigationTab.js +93 -0
  33. package/lib/components/NavigationTabs/NavigationTabContent.d.ts +7 -0
  34. package/lib/components/NavigationTabs/NavigationTabContent.js +50 -0
  35. package/lib/components/NavigationTabs/NavigationTabMenu.d.ts +7 -0
  36. package/lib/components/NavigationTabs/NavigationTabMenu.js +18 -0
  37. package/lib/components/NavigationTabs/NavigationTabs.d.ts +7 -0
  38. package/lib/components/NavigationTabs/NavigationTabs.js +97 -0
  39. package/lib/components/NavigationTabs/variables.d.ts +1 -0
  40. package/lib/components/NavigationTabs/variables.js +37 -0
  41. package/lib/components/Product/utils.js +1 -1
  42. package/lib/components/Search/SearchAiMessage.js +1 -1
  43. package/lib/components/Search/SearchAiResponse.js +1 -0
  44. package/lib/components/Search/SearchTrigger.js +8 -2
  45. package/lib/components/Sidebar/Sidebar.js +2 -2
  46. package/lib/components/TableOfContent/TableOfContent.js +2 -2
  47. package/lib/core/constants/common.d.ts +1 -0
  48. package/lib/core/constants/common.js +1 -0
  49. package/lib/core/constants/consent.d.ts +2 -0
  50. package/lib/core/constants/consent.js +30 -0
  51. package/lib/core/constants/index.d.ts +1 -0
  52. package/lib/core/constants/index.js +1 -0
  53. package/lib/core/hooks/index.d.ts +3 -0
  54. package/lib/core/hooks/index.js +3 -0
  55. package/lib/core/hooks/menu/use-mobile-menu-items.js +75 -51
  56. package/lib/core/hooks/menu/use-mobile-menu-levels.js +12 -7
  57. package/lib/core/hooks/use-consent-dialog.d.ts +12 -0
  58. package/lib/core/hooks/use-consent-dialog.js +36 -0
  59. package/lib/core/hooks/use-consent-telemetry.d.ts +1 -0
  60. package/lib/core/hooks/use-consent-telemetry.js +42 -0
  61. package/lib/core/hooks/use-scroll-item-into-view.d.ts +2 -0
  62. package/lib/core/hooks/use-scroll-item-into-view.js +16 -0
  63. package/lib/core/hooks/use-theme-hooks.js +16 -0
  64. package/lib/core/styles/global.js +54 -42
  65. package/lib/core/types/consent.d.ts +54 -0
  66. package/lib/core/types/consent.js +3 -0
  67. package/lib/core/types/hooks.d.ts +7 -0
  68. package/lib/core/types/index.d.ts +2 -0
  69. package/lib/core/types/l10n.d.ts +1 -1
  70. package/lib/core/types/navigation-tabs.d.ts +49 -0
  71. package/lib/core/types/navigation-tabs.js +3 -0
  72. package/lib/core/utils/consent-settings-link-placement.d.ts +3 -0
  73. package/lib/core/utils/consent-settings-link-placement.js +15 -0
  74. package/lib/core/utils/index.d.ts +2 -0
  75. package/lib/core/utils/index.js +2 -0
  76. package/lib/core/utils/mcp.js +1 -1
  77. package/lib/core/utils/menu.d.ts +4 -0
  78. package/lib/core/utils/menu.js +48 -8
  79. package/lib/core/utils/navigation-tabs.d.ts +8 -0
  80. package/lib/core/utils/navigation-tabs.js +71 -0
  81. package/lib/core/utils/urls.js +3 -1
  82. package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.d.ts +29 -0
  83. package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.js +81 -0
  84. package/lib/index.d.ts +6 -0
  85. package/lib/index.js +8 -1
  86. package/lib/layouts/PageLayout.js +1 -1
  87. package/lib/layouts/RootLayout.js +2 -2
  88. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +1 -1
  89. package/lib/markdoc/components/CodeWalkthrough/CodePanel.js +3 -3
  90. package/lib/markdoc/components/CodeWalkthrough/CodeWalkthrough.js +1 -1
  91. package/package.json +3 -3
  92. package/src/components/Catalog/Catalog.tsx +1 -1
  93. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +2 -2
  94. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +1 -1
  95. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanel.tsx +1 -1
  96. package/src/components/Catalog/CatalogMobileFiltersPanel/CatalogMobileFiltersPanelOverlay.tsx +2 -2
  97. package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
  98. package/src/components/Consent/Consent.tsx +18 -0
  99. package/src/components/Consent/ConsentBanner.tsx +110 -0
  100. package/src/components/Consent/ConsentDialog.tsx +210 -0
  101. package/src/components/Consent/ConsentSettingsLink.tsx +44 -0
  102. package/src/components/Consent/variables.ts +28 -0
  103. package/src/components/Dropdown/Dropdown.tsx +4 -1
  104. package/src/components/Footer/Footer.tsx +2 -0
  105. package/src/components/JsonViewer/Helpers.tsx +46 -51
  106. package/src/components/JsonViewer/JsonViewer.tsx +23 -25
  107. package/src/components/Markdown/Markdown.tsx +3 -2
  108. package/src/components/Markdown/styles/base-table.ts +14 -1
  109. package/src/components/Markdown/variables.ts +1 -0
  110. package/src/components/Menu/Menu.tsx +5 -0
  111. package/src/components/Menu/MenuSelect.tsx +198 -0
  112. package/src/components/Menu/variables.ts +43 -4
  113. package/src/components/Navbar/Navbar.tsx +22 -5
  114. package/src/components/Navbar/variables.ts +3 -0
  115. package/src/components/NavigationTabs/NavigationTab.tsx +148 -0
  116. package/src/components/NavigationTabs/NavigationTabContent.tsx +66 -0
  117. package/src/components/NavigationTabs/NavigationTabMenu.tsx +40 -0
  118. package/src/components/NavigationTabs/NavigationTabs.tsx +83 -0
  119. package/src/components/NavigationTabs/variables.ts +34 -0
  120. package/src/components/Product/utils.ts +1 -1
  121. package/src/components/Search/SearchAiMessage.tsx +1 -1
  122. package/src/components/Search/SearchAiResponse.tsx +1 -0
  123. package/src/components/Search/SearchTrigger.tsx +11 -2
  124. package/src/components/Sidebar/Sidebar.tsx +2 -2
  125. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  126. package/src/core/constants/common.ts +1 -0
  127. package/src/core/constants/consent.ts +29 -0
  128. package/src/core/constants/index.ts +1 -0
  129. package/src/core/hooks/__mocks__/use-theme-hooks.ts +17 -0
  130. package/src/core/hooks/index.ts +3 -0
  131. package/src/core/hooks/menu/use-mobile-menu-items.ts +91 -56
  132. package/src/core/hooks/menu/use-mobile-menu-levels.ts +14 -6
  133. package/src/core/hooks/use-consent-dialog.ts +51 -0
  134. package/src/core/hooks/use-consent-telemetry.ts +56 -0
  135. package/src/core/hooks/use-scroll-item-into-view.ts +18 -0
  136. package/src/core/hooks/use-theme-hooks.ts +16 -0
  137. package/src/core/styles/global.ts +14 -2
  138. package/src/core/types/consent.ts +51 -0
  139. package/src/core/types/hooks.ts +7 -0
  140. package/src/core/types/index.ts +2 -0
  141. package/src/core/types/l10n.ts +25 -1
  142. package/src/core/types/navigation-tabs.ts +54 -0
  143. package/src/core/utils/consent-settings-link-placement.ts +18 -0
  144. package/src/core/utils/index.ts +2 -0
  145. package/src/core/utils/mcp.ts +1 -1
  146. package/src/core/utils/menu.ts +54 -9
  147. package/src/core/utils/navigation-tabs.ts +99 -0
  148. package/src/core/utils/urls.ts +3 -1
  149. package/src/icons/ExpandCollapseIcon/ExpandCollapseIcon.tsx +56 -0
  150. package/src/index.ts +7 -0
  151. package/src/layouts/PageLayout.tsx +1 -1
  152. package/src/layouts/RootLayout.tsx +2 -2
  153. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +1 -1
  154. package/src/markdoc/components/CodeWalkthrough/CodePanel.tsx +3 -3
  155. package/src/markdoc/components/CodeWalkthrough/CodeWalkthrough.tsx +1 -1
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import type { JSX } from 'react';
5
+ import type { NavigationTabItem } from '@redocly/theme/core/types';
6
+
7
+ import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
8
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
9
+ import { NavigationTabContent } from '@redocly/theme/components/NavigationTabs/NavigationTabContent';
10
+
11
+ export type NavigationTabMenuProps = {
12
+ items: NavigationTabItem[];
13
+ className?: string;
14
+ };
15
+
16
+ export function NavigationTabMenu({ items, className }: NavigationTabMenuProps): JSX.Element {
17
+ return (
18
+ <NavigationTabMenuWrapper
19
+ data-component-name="NavigationTabs/NavigationTabMenu"
20
+ className={className}
21
+ >
22
+ <DropdownMenu>
23
+ {items.map((item, index) => (
24
+ <DropdownMenuItem
25
+ key={`${item.label}_${index}`}
26
+ to={item.link}
27
+ external={item.external}
28
+ data-translation-key={item.labelTranslationKey}
29
+ active={item.active}
30
+ aria-current={item.active ? 'page' : undefined}
31
+ >
32
+ <NavigationTabContent item={item} />
33
+ </DropdownMenuItem>
34
+ ))}
35
+ </DropdownMenu>
36
+ </NavigationTabMenuWrapper>
37
+ );
38
+ }
39
+
40
+ const NavigationTabMenuWrapper = styled.div``;
@@ -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
+ `;
@@ -2,5 +2,5 @@ export function getProductClassName(productName: string): string {
2
2
  return `product-${productName
3
3
  .toLowerCase()
4
4
  .replace(/[^a-z0-9]+/g, '-')
5
- .replace(/^-+|-+$/g, '')}`;
5
+ .replace(/^-+|(?<!-)-+$/g, '')}`;
6
6
  }
@@ -347,7 +347,7 @@ const ResourceTagsWrapper = styled.div`
347
347
  gap: var(--search-ai-resource-tags-gap);
348
348
  `;
349
349
 
350
- const ResourceTag = styled(Tag)`
350
+ const ResourceTag = styled(Tag).attrs({ textTransform: 'none' })`
351
351
  .tag-default {
352
352
  --tag-color: var(--search-ai-resource-tag-text-color);
353
353
  max-width: 100%;
@@ -212,6 +212,7 @@ const ResourceTags = styled.div`
212
212
 
213
213
  const ResourceTag = styled(Tag).attrs({
214
214
  className: 'tag-resource',
215
+ textTransform: 'none',
215
216
  })`
216
217
  .tag-default {
217
218
  --tag-color: --search-ai-resource-tag-text-color;
@@ -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',
@@ -0,0 +1,29 @@
1
+ import type { ConsentCategory } from '../types/consent';
2
+
3
+ export const CONSENT_CATEGORIES: readonly ConsentCategory[] = [
4
+ {
5
+ id: 'necessary',
6
+ locked: true,
7
+ titleTranslationKey: 'consent.category.necessary.title',
8
+ descriptionTranslationKey: 'consent.category.necessary.description',
9
+ titleDefault: 'Necessary',
10
+ descriptionDefault:
11
+ 'Needed for the site to work: your consent choice, your color mode, and anonymous usage data.',
12
+ },
13
+ {
14
+ id: 'analytics',
15
+ locked: false,
16
+ titleTranslationKey: 'consent.category.analytics.title',
17
+ descriptionTranslationKey: 'consent.category.analytics.description',
18
+ titleDefault: 'Analytics',
19
+ descriptionDefault: 'Helps us understand which pages are useful.',
20
+ },
21
+ {
22
+ id: 'marketing',
23
+ locked: false,
24
+ titleTranslationKey: 'consent.category.marketing.title',
25
+ descriptionTranslationKey: 'consent.category.marketing.description',
26
+ titleDefault: 'Marketing',
27
+ descriptionDefault: 'Lets us measure which campaigns bring visitors to this site.',
28
+ },
29
+ ];
@@ -10,3 +10,4 @@ export * from './mcp';
10
10
  export * from './feedback';
11
11
  export * from './heading';
12
12
  export * from './markdown';
13
+ export * from './consent';
@@ -19,12 +19,27 @@ export const useThemeHooks = vi.fn(() => ({
19
19
  sendMcpConnectionSnippetCopiedMessage: vi.fn(),
20
20
  sendPageActionsButtonClickedMessage: vi.fn(),
21
21
  sendColorModeSwitchedMessage: vi.fn(),
22
+ sendConsentChangedMessage: vi.fn(),
22
23
  })),
23
24
  useBreadcrumbs: vi.fn().mockReturnValue({ breadcrumbs: [], siblings: undefined }),
24
25
  useBanner: vi.fn(() => ({
25
26
  banner: undefined,
26
27
  dismissBanner: vi.fn(),
27
28
  })),
29
+ useConsent: vi.fn(() => ({
30
+ enabled: false,
31
+ config: undefined,
32
+ mode: undefined,
33
+ state: undefined,
34
+ decided: false,
35
+ gpc: false,
36
+ isDialogOpen: false,
37
+ openDialog: vi.fn(),
38
+ closeDialog: vi.fn(),
39
+ accept: vi.fn(),
40
+ reject: vi.fn(),
41
+ save: vi.fn(),
42
+ })),
28
43
  usePageSharedData: vi.fn().mockReturnValue({}),
29
44
  useCatalogClassic: vi.fn(() => ({
30
45
  groups: [],
@@ -55,6 +70,7 @@ export const useThemeHooks = vi.fn(() => ({
55
70
  items: [],
56
71
  isLoading: false,
57
72
  })),
73
+ useSearchFeatures: vi.fn(() => ({ askAi: false })),
58
74
  useAiSearch: vi.fn(() => ({
59
75
  askQuestion: vi.fn(),
60
76
  isGeneratingResponse: false,
@@ -102,6 +118,7 @@ export const useThemeHooks = vi.fn(() => ({
102
118
  pushDrilldownState: vi.fn(),
103
119
  }),
104
120
  useSidebarSiblingsData: vi.fn(),
121
+ useNavigationTabs: vi.fn(),
105
122
  useSubmitFeedback: vi.fn(() => ({
106
123
  submitFeedback: vi.fn(),
107
124
  })),
@@ -22,6 +22,8 @@ export * from './menu/use-collapse';
22
22
  export * from './feedback/use-report-dialog';
23
23
  export * from './use-tabs';
24
24
  export * from './use-focus-trap';
25
+ export * from './use-consent-dialog';
26
+ export * from './use-consent-telemetry';
25
27
  export * from './use-language-picker';
26
28
  export * from './use-product-picker';
27
29
  export * from './search/use-search-dialog';
@@ -56,3 +58,4 @@ export * from './use-toast-logic';
56
58
  export * from './use-banner-telemetry';
57
59
  export * from './use-tooltip-fallback-placement';
58
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,51 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ import type { RefObject } from 'react';
4
+ import type { ConsentCategory, ConsentCategoryId, UseConsentResult } from '../types/consent';
5
+
6
+ import { CONSENT_CATEGORIES } from '../constants/consent';
7
+ import { useFocusTrap } from './use-focus-trap';
8
+
9
+ export type ConsentDraft = Record<Exclude<ConsentCategoryId, 'necessary'>, boolean>;
10
+
11
+ export type UseConsentDialogResult = {
12
+ draft: ConsentDraft;
13
+ setCategory: (id: keyof ConsentDraft, value: boolean) => void;
14
+ dialogRef: RefObject<HTMLDivElement | null>;
15
+ close: () => void;
16
+ save: () => void;
17
+ categories: readonly ConsentCategory[];
18
+ };
19
+
20
+ export function useConsentDialog(consent: UseConsentResult): UseConsentDialogResult {
21
+ const [draft, setDraft] = useState<ConsentDraft>(() => ({
22
+ analytics: consent.state?.analytics ?? consent.mode === 'opt-out',
23
+ marketing: (consent.state?.marketing ?? consent.mode === 'opt-out') && !consent.gpc,
24
+ }));
25
+ const dialogRef = useRef<HTMLDivElement | null>(null);
26
+ const close = consent.closeDialog;
27
+ const closeRef = useRef(close);
28
+ closeRef.current = close;
29
+
30
+ useEffect(() => {
31
+ const previous = document.activeElement as HTMLElement | null;
32
+ const onKeyDown = (event: KeyboardEvent) => {
33
+ if (event.key === 'Escape') closeRef.current();
34
+ };
35
+ document.addEventListener('keydown', onKeyDown);
36
+ return () => {
37
+ document.removeEventListener('keydown', onKeyDown);
38
+ previous?.focus?.();
39
+ };
40
+ }, []);
41
+
42
+ useFocusTrap(dialogRef);
43
+
44
+ const setCategory = (id: keyof ConsentDraft, value: boolean) => {
45
+ setDraft((current) => ({ ...current, [id]: value }));
46
+ };
47
+
48
+ const save = () => consent.save(draft);
49
+
50
+ return { draft, setCategory, dialogRef, close, save, categories: CONSENT_CATEGORIES };
51
+ }