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

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 (97) hide show
  1. package/lib/components/CodeBlock/CodeBlockControls.js +3 -5
  2. package/lib/components/Consent/Consent.d.ts +2 -0
  3. package/lib/components/Consent/Consent.js +17 -0
  4. package/lib/components/Consent/ConsentBanner.d.ts +2 -0
  5. package/lib/components/Consent/ConsentBanner.js +120 -0
  6. package/lib/components/Consent/ConsentDialog.d.ts +2 -0
  7. package/lib/components/Consent/ConsentDialog.js +128 -0
  8. package/lib/components/Consent/ConsentSettingsLink.d.ts +2 -0
  9. package/lib/components/Consent/ConsentSettingsLink.js +35 -0
  10. package/lib/components/Consent/variables.d.ts +1 -0
  11. package/lib/components/Consent/variables.js +31 -0
  12. package/lib/components/Footer/Footer.js +4 -1
  13. package/lib/components/JsonViewer/Helpers.d.ts +10 -2
  14. package/lib/components/JsonViewer/Helpers.js +29 -33
  15. package/lib/components/JsonViewer/JsonViewer.js +15 -18
  16. package/lib/components/Markdown/styles/base-table.js +14 -1
  17. package/lib/components/Markdown/variables.js +1 -0
  18. package/lib/components/Product/utils.js +1 -1
  19. package/lib/components/Search/SearchAiConversationInput.js +10 -6
  20. package/lib/components/Search/SearchAiDialog.js +8 -1
  21. package/lib/components/Search/SearchAiMessage.js +1 -1
  22. package/lib/components/Search/SearchAiResponse.js +1 -0
  23. package/lib/components/Search/SearchItem.js +9 -1
  24. package/lib/core/constants/consent.d.ts +2 -0
  25. package/lib/core/constants/consent.js +30 -0
  26. package/lib/core/constants/index.d.ts +1 -0
  27. package/lib/core/constants/index.js +1 -0
  28. package/lib/core/constants/search.d.ts +1 -1
  29. package/lib/core/constants/search.js +1 -1
  30. package/lib/core/hooks/index.d.ts +2 -0
  31. package/lib/core/hooks/index.js +2 -0
  32. package/lib/core/hooks/use-consent-dialog.d.ts +12 -0
  33. package/lib/core/hooks/use-consent-dialog.js +36 -0
  34. package/lib/core/hooks/use-consent-telemetry.d.ts +1 -0
  35. package/lib/core/hooks/use-consent-telemetry.js +42 -0
  36. package/lib/core/hooks/use-dialog-hotkeys.js +4 -2
  37. package/lib/core/hooks/use-theme-hooks.js +14 -0
  38. package/lib/core/styles/global.js +42 -40
  39. package/lib/core/types/consent.d.ts +54 -0
  40. package/lib/core/types/consent.js +3 -0
  41. package/lib/core/types/hooks.d.ts +2 -0
  42. package/lib/core/types/index.d.ts +1 -0
  43. package/lib/core/types/l10n.d.ts +1 -1
  44. package/lib/core/types/search.d.ts +2 -0
  45. package/lib/core/utils/consent-settings-link-placement.d.ts +3 -0
  46. package/lib/core/utils/consent-settings-link-placement.js +15 -0
  47. package/lib/core/utils/index.d.ts +1 -0
  48. package/lib/core/utils/index.js +1 -0
  49. package/lib/core/utils/mcp.js +1 -1
  50. package/lib/core/utils/menu.js +6 -5
  51. package/lib/core/utils/urls.js +3 -1
  52. package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.d.ts +29 -0
  53. package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.js +81 -0
  54. package/lib/index.d.ts +1 -0
  55. package/lib/index.js +1 -0
  56. package/package.json +3 -3
  57. package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
  58. package/src/components/Consent/Consent.tsx +18 -0
  59. package/src/components/Consent/ConsentBanner.tsx +110 -0
  60. package/src/components/Consent/ConsentDialog.tsx +210 -0
  61. package/src/components/Consent/ConsentSettingsLink.tsx +44 -0
  62. package/src/components/Consent/variables.ts +28 -0
  63. package/src/components/Footer/Footer.tsx +2 -0
  64. package/src/components/JsonViewer/Helpers.tsx +46 -51
  65. package/src/components/JsonViewer/JsonViewer.tsx +23 -25
  66. package/src/components/Markdown/styles/base-table.ts +14 -1
  67. package/src/components/Markdown/variables.ts +1 -0
  68. package/src/components/Product/utils.ts +1 -1
  69. package/src/components/Search/SearchAiConversationInput.tsx +13 -7
  70. package/src/components/Search/SearchAiDialog.tsx +16 -1
  71. package/src/components/Search/SearchAiMessage.tsx +1 -1
  72. package/src/components/Search/SearchAiResponse.tsx +1 -0
  73. package/src/components/Search/SearchItem.tsx +14 -0
  74. package/src/core/constants/consent.ts +29 -0
  75. package/src/core/constants/index.ts +1 -0
  76. package/src/core/constants/search.ts +1 -1
  77. package/src/core/hooks/__mocks__/use-theme-hooks.ts +15 -0
  78. package/src/core/hooks/code-walkthrough/__mocks__/MockIntersectionObserver.ts +1 -2
  79. package/src/core/hooks/code-walkthrough/__mocks__/mock-intersection-observer.ts +1 -2
  80. package/src/core/hooks/index.ts +2 -0
  81. package/src/core/hooks/use-consent-dialog.ts +51 -0
  82. package/src/core/hooks/use-consent-telemetry.ts +56 -0
  83. package/src/core/hooks/use-dialog-hotkeys.ts +11 -5
  84. package/src/core/hooks/use-theme-hooks.ts +14 -0
  85. package/src/core/styles/global.ts +2 -0
  86. package/src/core/types/consent.ts +51 -0
  87. package/src/core/types/hooks.ts +2 -0
  88. package/src/core/types/index.ts +1 -0
  89. package/src/core/types/l10n.ts +25 -1
  90. package/src/core/types/search.ts +2 -0
  91. package/src/core/utils/consent-settings-link-placement.ts +18 -0
  92. package/src/core/utils/index.ts +1 -0
  93. package/src/core/utils/mcp.ts +1 -1
  94. package/src/core/utils/menu.ts +5 -6
  95. package/src/core/utils/urls.ts +3 -1
  96. package/src/icons/ExpandCollapseIcon/ExpandCollapseIcon.tsx +56 -0
  97. package/src/index.ts +1 -0
@@ -58,6 +58,17 @@ export function SearchAiDialog({
58
58
  const { translate } = useTranslate();
59
59
 
60
60
  const conversationEndRef = React.useRef<HTMLDivElement>(null);
61
+ const wrapperRef = React.useRef<HTMLDivElement>(null);
62
+
63
+ useEffect(() => {
64
+ requestAnimationFrame(() => {
65
+ wrapperRef.current
66
+ ?.querySelector<HTMLElement>(
67
+ '[data-component-name="Search/SearchAiConversationInput"] :is(input, textarea):not(:disabled)',
68
+ )
69
+ ?.focus();
70
+ });
71
+ }, []);
61
72
 
62
73
  const suggestions = aiAssistant?.suggestions ?? [];
63
74
 
@@ -167,7 +178,11 @@ export function SearchAiDialog({
167
178
  );
168
179
 
169
180
  return (
170
- <SearchAiDialogWrapper data-component-name="Search/SearchAiDialog" className={className}>
181
+ <SearchAiDialogWrapper
182
+ ref={wrapperRef}
183
+ data-component-name="Search/SearchAiDialog"
184
+ className={className}
185
+ >
171
186
  {!conversation.length && (
172
187
  <WelcomeWrapper>
173
188
  <AiStarsIcon
@@ -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;
@@ -123,6 +123,20 @@ export function SearchItem({
123
123
  {searchHighlight(parameter.name)}
124
124
  </div>
125
125
  <div>{searchHighlight(parameter.description)}</div>
126
+ {parameter.enum ? (
127
+ <div>
128
+ {translate('search.searchItem.enum', 'Enum')}
129
+ {': '}
130
+ {searchHighlight(parameter.enum)}
131
+ </div>
132
+ ) : null}
133
+ {parameter.example ? (
134
+ <div>
135
+ {translate('search.searchItem.example', 'Example')}
136
+ {': '}
137
+ {searchHighlight(parameter.example)}
138
+ </div>
139
+ ) : null}
126
140
  </SearchItemPlace>
127
141
  ) : (
128
142
  shouldShowPath && (
@@ -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';
@@ -44,7 +44,7 @@ export const AI_SEARCH_ERROR_CONFIG: Record<AiSearchError, AiSearchErrorConfig>
44
44
 
45
45
  export const AI_SEARCH_MAX_MESSAGE_LENGTH = 45000;
46
46
 
47
- export const SEARCH_DEBOUNCE_TIME_MS = 300;
47
+ export const SEARCH_DEBOUNCE_TIME_MS = 500;
48
48
 
49
49
  export const DEFAULT_KEYBOARD_SHORTCUTS = '⌘+K,CTRL+K';
50
50
 
@@ -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: [],
@@ -8,8 +8,7 @@ export class MockIntersectionObserver {
8
8
  unobserve: (target: HTMLElement) => void;
9
9
  disconnect: () => void;
10
10
 
11
- // @ts-expect-error
12
- constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
11
+ constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {
13
12
  this.root = null;
14
13
  this.rootMargin = '0px';
15
14
  this.thresholds = [1];
@@ -9,8 +9,7 @@ export class MockIntersectionObserver {
9
9
  unobserve: (target: HTMLElement) => void;
10
10
  disconnect: () => void;
11
11
 
12
- // @ts-expect-error
13
- constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
12
+ constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {
14
13
  this.root = null;
15
14
  this.rootMargin = '0px';
16
15
  this.scrollMargin = '';
@@ -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';
@@ -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
+ }
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import type { ConsentRuntime } from '../types/consent';
4
+
5
+ import { useThemeHooks } from './use-theme-hooks';
6
+ import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
7
+
8
+ type RealmOwnedRuntime = ConsentRuntime & {
9
+ mode: 'opt-in' | 'opt-out';
10
+ configuredMode: 'auto' | 'opt-in' | 'opt-out';
11
+ region: { source: 'timezone' | 'config' | 'none'; timeZone: string | null };
12
+ };
13
+
14
+ function isRealmOwned(runtime: ConsentRuntime): runtime is RealmOwnedRuntime {
15
+ return runtime.region.source !== 'provider';
16
+ }
17
+
18
+ function getConsentRuntime(): ConsentRuntime | undefined {
19
+ if (typeof window === 'undefined') return undefined;
20
+ return (window as Window & { redocly?: { consent?: ConsentRuntime } }).redocly?.consent;
21
+ }
22
+
23
+ // Sends one consent.changed event per visitor choice in the modes Realm owns.
24
+ export function useConsentTelemetry(): void {
25
+ const { useTelemetry } = useThemeHooks();
26
+ const telemetry = useTelemetry();
27
+ const telemetryRef = useRef(telemetry);
28
+ telemetryRef.current = telemetry;
29
+
30
+ useEffect(() => {
31
+ const found = getConsentRuntime();
32
+ const runtime = found && isRealmOwned(found) ? found : null;
33
+ if (!runtime) return;
34
+ return runtime.on('change', (state) => {
35
+ const choice =
36
+ state.analytics && state.marketing
37
+ ? 'accept'
38
+ : !state.analytics && !state.marketing
39
+ ? 'reject'
40
+ : 'partial';
41
+ const { origin, pathname, search, hash } = window.location;
42
+ const uri = `${origin}${pathname}${search}${hash}`;
43
+ telemetryRef.current.sendConsentChangedMessage([
44
+ {
45
+ ...getBaseDataAttributes('consent', 'consent'),
46
+ choice,
47
+ mode: runtime.mode,
48
+ configuredMode: runtime.configuredMode,
49
+ regionSource: runtime.region.source,
50
+ gpc: runtime.gpc,
51
+ },
52
+ getBaseDataAttributes(uri, 'page', uri),
53
+ ]);
54
+ });
55
+ }, []);
56
+ }
@@ -21,12 +21,18 @@ export function useDialogHotKeys(ref: RefObject<HTMLElement | null>, onClose: ()
21
21
  };
22
22
 
23
23
  const onFocusableElementsHandler = () => {
24
- const focusableElements = ref.current?.querySelectorAll(
25
- 'input, [href], [tabindex]:not([tabindex="-1"])',
24
+ const focusableElements = Array.from(
25
+ ref.current?.querySelectorAll<HTMLElement>(
26
+ 'input, textarea, select, button, [href], [tabindex]:not([tabindex="-1"])',
27
+ ) ?? [],
28
+ ).filter(
29
+ (element) =>
30
+ !element.hasAttribute('disabled') &&
31
+ (typeof element.checkVisibility !== 'function' || element.checkVisibility()),
26
32
  );
27
- if (focusableElements && focusableElements.length > 0) {
28
- firstFocusableRef.current = focusableElements[0] as HTMLElement;
29
- lastFocusableRef.current = focusableElements[focusableElements.length - 1] as HTMLElement;
33
+ if (focusableElements.length > 0) {
34
+ firstFocusableRef.current = focusableElements[0];
35
+ lastFocusableRef.current = focusableElements[focusableElements.length - 1];
30
36
  }
31
37
  };
32
38
 
@@ -19,6 +19,20 @@ const fallbacks = {
19
19
  useOtelTelemetry: () => ({ send: () => {} }),
20
20
  useBreadcrumbs: () => ({ breadcrumbs: [], siblings: undefined }),
21
21
  useBanner: () => ({ banner: undefined, dismissBanner: () => {} }),
22
+ useConsent: () => ({
23
+ enabled: false,
24
+ config: undefined,
25
+ mode: undefined,
26
+ state: undefined,
27
+ decided: false,
28
+ gpc: false,
29
+ isDialogOpen: false,
30
+ openDialog: () => {},
31
+ closeDialog: () => {},
32
+ accept: () => {},
33
+ reject: () => {},
34
+ save: () => {},
35
+ }),
22
36
  useCodeHighlight: () => ({ highlight: (rawContent: string) => rawContent }),
23
37
  useUserMenu: () => ({}),
24
38
  usePageData: () => null,
@@ -28,6 +28,7 @@ import { code } from '@redocly/theme/components/CodeBlock/variables';
28
28
  import { productPicker } from '@redocly/theme/components/Product/variables';
29
29
  import { markdown } from '@redocly/theme/components/Markdown/variables';
30
30
  import { banner } from '@redocly/theme/components/Banner/variables';
31
+ import { consent } from '@redocly/theme/components/Consent/variables';
31
32
  import { markdownAccordion } from '@redocly/theme/markdoc/components/Accordion/variables';
32
33
  import { markdownTabs } from '@redocly/theme/markdoc/components/Tabs/variables';
33
34
  import { markdownNumberedList } from '@redocly/theme/markdoc/components/NumberedList/variables';
@@ -1336,6 +1337,7 @@ export const styles = css<{ palette?: string }>`
1336
1337
  ${badges}
1337
1338
  ${banner}
1338
1339
  ${borders}
1340
+ ${consent}
1339
1341
  ${breadcrumbs}
1340
1342
  ${button}
1341
1343
  ${aiAssistantButton}
@@ -0,0 +1,51 @@
1
+ import type { ConsentConfig } from '@redocly/config';
2
+ import type { TranslationKey } from './l10n';
3
+
4
+ export type ConsentCategoryId = 'necessary' | 'analytics' | 'marketing';
5
+
6
+ export type ConsentCategory = {
7
+ id: ConsentCategoryId;
8
+ locked: boolean;
9
+ titleTranslationKey: TranslationKey;
10
+ descriptionTranslationKey: TranslationKey;
11
+ titleDefault: string;
12
+ descriptionDefault: string;
13
+ };
14
+
15
+ export type ConsentState = { necessary: true; analytics: boolean; marketing: boolean };
16
+
17
+ export type ConsentChoice = { analytics?: boolean; marketing?: boolean };
18
+
19
+ export type ConsentProvider = 'onetrust' | 'cookiebot' | 'trustarc' | 'tcf';
20
+
21
+ export type ConsentRuntime = {
22
+ configuredMode: 'auto' | 'opt-in' | 'opt-out' | ConsentProvider;
23
+ mode: 'opt-in' | 'opt-out' | ConsentProvider;
24
+ state: ConsentState;
25
+ decided: boolean;
26
+ gpc: boolean;
27
+ dialogOpen: boolean;
28
+ region: { source: 'timezone' | 'config' | 'none' | 'provider'; timeZone: string | null };
29
+ accept: () => void;
30
+ reject: () => void;
31
+ save: (next: ConsentChoice) => void;
32
+ open: () => void;
33
+ close: () => void;
34
+ reload: () => void;
35
+ on: (event: 'change' | 'open' | 'close', fn: (state: ConsentState) => void) => () => void;
36
+ };
37
+
38
+ export type UseConsentResult = {
39
+ enabled: boolean;
40
+ config: ConsentConfig | undefined;
41
+ mode: ConsentRuntime['mode'] | undefined;
42
+ state: ConsentState | undefined;
43
+ decided: boolean;
44
+ gpc: boolean;
45
+ isDialogOpen: boolean;
46
+ openDialog: () => void;
47
+ closeDialog: () => void;
48
+ accept: () => void;
49
+ reject: () => void;
50
+ save: (next: ConsentChoice) => void;
51
+ };
@@ -40,6 +40,7 @@ import type {
40
40
  import type { SubmitFeedbackParams } from './feedback';
41
41
  import type { TFunction } from './l10n';
42
42
  import type { BreadcrumbItem } from './breadcrumb';
43
+ import type { UseConsentResult } from './consent';
43
44
 
44
45
  import type { AiSearchError } from '../constants/search';
45
46
 
@@ -90,6 +91,7 @@ export type ThemeHooks = {
90
91
  banner: ResolvedBannerConfig | undefined;
91
92
  dismissBanner: (content: string) => void;
92
93
  };
94
+ useConsent: () => UseConsentResult;
93
95
  useSearch: (
94
96
  product?: string,
95
97
  autoSearchDisabled?: boolean,
@@ -1,5 +1,6 @@
1
1
  export type * from './breadcrumb';
2
2
  export type * from './catalog';
3
+ export type * from './consent';
3
4
  export type * from './l10n';
4
5
  export type * from './mcp';
5
6
  export type * from './feedback';
@@ -102,6 +102,7 @@ export type TranslationKey =
102
102
  | 'search.ai.placeholder'
103
103
  | 'search.ai.generatingResponse'
104
104
  | 'search.ai.followUpQuestion'
105
+ | 'search.ai.send'
105
106
  | 'search.ai.suggestionsTitle'
106
107
  | 'search.ai.thinkingText'
107
108
  | 'search.ai.resourcesFound'
@@ -124,6 +125,8 @@ export type TranslationKey =
124
125
  | 'search.ai.error.header.unauthorized'
125
126
  | 'search.ai.feedback.more'
126
127
  | 'search.searchItem.deprecated'
128
+ | 'search.searchItem.enum'
129
+ | 'search.searchItem.example'
127
130
  | 'search.groups.all'
128
131
  | 'search.filter.field.footer'
129
132
  | 'aiAssistant.trigger'
@@ -510,7 +513,28 @@ export type TranslationKey =
510
513
  | 'diagram.zoomOut'
511
514
  | 'diagram.reset'
512
515
  | 'diagram.close'
513
- | 'diagram.viewer';
516
+ | 'diagram.viewer'
517
+ | 'consent.title'
518
+ | 'consent.message'
519
+ | 'consent.privacyPolicy'
520
+ | 'consent.customize'
521
+ | 'consent.rejectAll'
522
+ | 'consent.acceptAll'
523
+ | 'consent.acceptAllCookies'
524
+ | 'consent.savePreferences'
525
+ | 'consent.dialog.title'
526
+ | 'consent.dialog.description'
527
+ | 'consent.dialog.close'
528
+ | 'consent.alwaysOn'
529
+ | 'consent.alwaysOff'
530
+ | 'consent.gpcNotice'
531
+ | 'consent.category.necessary.title'
532
+ | 'consent.category.necessary.description'
533
+ | 'consent.category.analytics.title'
534
+ | 'consent.category.analytics.description'
535
+ | 'consent.category.marketing.title'
536
+ | 'consent.category.marketing.description'
537
+ | 'consent.settingsLink';
514
538
 
515
539
  export type Locale = { code: string; name: string };
516
540
 
@@ -45,6 +45,8 @@ export type ParameterHighlight = {
45
45
  description: string;
46
46
  place: string;
47
47
  path: string[];
48
+ enum?: string;
49
+ example?: string;
48
50
  };
49
51
 
50
52
  export type SearchItemData = {
@@ -0,0 +1,18 @@
1
+ import type { ConsentConfig, RedoclyConfig } from '@redocly/config';
2
+
3
+ export type SettingsLinkPlacement = 'footer' | 'none';
4
+
5
+ export function resolveSettingsLinkPlacement(
6
+ consent: ConsentConfig | undefined,
7
+ footer: RedoclyConfig['footer'] | undefined,
8
+ ): SettingsLinkPlacement {
9
+ const choice = consent?.settingsLink ?? 'auto';
10
+ if (choice !== 'auto') {
11
+ return choice;
12
+ }
13
+ const footerShown =
14
+ Boolean(footer) &&
15
+ !footer?.hide &&
16
+ (Boolean(footer?.copyrightText) || (Array.isArray(footer?.items) && footer.items.length > 0));
17
+ return footerShown ? 'footer' : 'none';
18
+ }
@@ -49,3 +49,4 @@ export * from './tooltip-placement';
49
49
  export * from './telemetry/get-base-data-attributes';
50
50
  export * from './telemetry/generate-before-after-context';
51
51
  export * from './RenderWithLink';
52
+ export * from './consent-settings-link-placement';
@@ -39,7 +39,7 @@ export function slugifyMcpServerName(name: string): string {
39
39
  return name
40
40
  .toLowerCase()
41
41
  .replace(/[^a-z0-9_-]+/g, '-')
42
- .replace(/^-+|-+$/g, '');
42
+ .replace(/^-+|(?<!-)-+$/g, '');
43
43
  }
44
44
 
45
45
  export function generateClaudeCodeCommand(config: McpConnectionParams): string {
@@ -24,10 +24,7 @@ export const mapNavbarItems = (
24
24
  ...(navItem.items && {
25
25
  items: mapNavbarItems(navItem.items, defaultLocale, currentLocale, locales, location),
26
26
  }),
27
- ...('link' in navItem && { link: navItem.link || '/' }),
28
- active:
29
- 'link' in navItem &&
30
- isItemActive(navItem, defaultLocale, currentLocale, locales, location),
27
+ active: isItemActive(navItem, defaultLocale, currentLocale, locales, location),
31
28
  hasActiveSubItem: !!navItem.items?.find((item) =>
32
29
  isItemActive(item, defaultLocale, currentLocale, locales, location),
33
30
  ),
@@ -42,10 +39,12 @@ export const isItemActive = (
42
39
  locales: Locale[],
43
40
  location: Location,
44
41
  ): boolean => {
42
+ if (!item.link) return false;
43
+
45
44
  const pathHash = location.pathname + location.hash;
46
45
  const link = item.languageInsensitive
47
- ? item.link || ''
48
- : getPathnameForLocale(item.link || '/', defaultLocale, currentLocale, locales);
46
+ ? item.link
47
+ : getPathnameForLocale(item.link, defaultLocale, currentLocale, locales);
49
48
 
50
49
  return pathHash === withPathPrefix(link);
51
50
  };
@@ -5,7 +5,9 @@ import { DEFAULT_LOCALE_PLACEHOLDER } from '../constants/common';
5
5
  export function combineUrls(baseURL: string, ...relativeURLs: (string | undefined)[]) {
6
6
  let res = baseURL;
7
7
  for (let relativeURL of relativeURLs) {
8
- res = relativeURL ? res.replace(/[/\\]+$/, '') + '/' + relativeURL.replace(/^[/\\]+/, '') : res;
8
+ res = relativeURL
9
+ ? res.replace(/(?<![/\\])[/\\]+$/, '') + '/' + relativeURL.replace(/^[/\\]+/, '')
10
+ : res;
9
11
  }
10
12
 
11
13
  return res;
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import styled, { css } from 'styled-components';
3
+
4
+ import type { IconProps } from '@redocly/theme/icons/types';
5
+
6
+ import { getCssColorVariable } from '@redocly/theme/core/utils';
7
+
8
+ const Icon = (props: IconProps) => (
9
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
10
+ <path
11
+ className="corner-top"
12
+ d="M12.2432 8.70703H11.2461L11.2422 4.75781L7.29297 4.75391V3.75781H12.2432V8.70703Z"
13
+ />
14
+ <path
15
+ className="corner-bottom"
16
+ d="M4.75781 11.2422L8.70703 11.2461V12.2432H3.75781V7.29297H4.75391L4.75781 11.2422Z"
17
+ />
18
+ </svg>
19
+ );
20
+
21
+ export const ExpandCollapseIcon = styled(Icon).attrs({
22
+ 'data-component-name': 'icons/ExpandCollapseIcon/ExpandCollapseIcon',
23
+ })<{ $collapse?: boolean }>`
24
+ path {
25
+ fill: ${({ color }) => getCssColorVariable(color)};
26
+ transform-box: view-box;
27
+ transition: transform 0.3s ease-in-out;
28
+
29
+ @media (prefers-reduced-motion: reduce) {
30
+ transition: none;
31
+ }
32
+ }
33
+
34
+ .corner-top {
35
+ transform-origin: 10.756px 5.244px;
36
+ }
37
+
38
+ .corner-bottom {
39
+ transform-origin: 5.244px 10.756px;
40
+ }
41
+
42
+ ${({ $collapse }) =>
43
+ $collapse &&
44
+ css`
45
+ .corner-top {
46
+ transform: rotate(180deg);
47
+ }
48
+
49
+ .corner-bottom {
50
+ transform: rotate(-180deg);
51
+ }
52
+ `}
53
+
54
+ height: ${({ size }) => size || '16px'};
55
+ width: ${({ size }) => size || '16px'};
56
+ `;
package/src/index.ts CHANGED
@@ -253,6 +253,7 @@ export * from '@redocly/theme/icons/LinkIcon/LinkIcon';
253
253
  export * from '@redocly/theme/icons/LaunchIcon/LaunchIcon';
254
254
  export * from '@redocly/theme/icons/MinimizeIcon/MinimizeIcon';
255
255
  export * from '@redocly/theme/icons/MaximizeIcon/MaximizeIcon';
256
+ export * from '@redocly/theme/icons/ExpandCollapseIcon/ExpandCollapseIcon';
256
257
  export * from '@redocly/theme/icons/MarkdownFullIcon/MarkdownFullIcon';
257
258
  export * from '@redocly/theme/icons/DocumentBlankIcon/DocumentBlankIcon';
258
259
  export * from '@redocly/theme/icons/WarningSquareIcon/WarningSquareIcon';