@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.
- package/lib/components/CodeBlock/CodeBlockControls.js +3 -5
- package/lib/components/Consent/Consent.d.ts +2 -0
- package/lib/components/Consent/Consent.js +17 -0
- package/lib/components/Consent/ConsentBanner.d.ts +2 -0
- package/lib/components/Consent/ConsentBanner.js +120 -0
- package/lib/components/Consent/ConsentDialog.d.ts +2 -0
- package/lib/components/Consent/ConsentDialog.js +128 -0
- package/lib/components/Consent/ConsentSettingsLink.d.ts +2 -0
- package/lib/components/Consent/ConsentSettingsLink.js +35 -0
- package/lib/components/Consent/variables.d.ts +1 -0
- package/lib/components/Consent/variables.js +31 -0
- package/lib/components/Footer/Footer.js +4 -1
- package/lib/components/JsonViewer/Helpers.d.ts +10 -2
- package/lib/components/JsonViewer/Helpers.js +29 -33
- package/lib/components/JsonViewer/JsonViewer.js +15 -18
- package/lib/components/Markdown/styles/base-table.js +14 -1
- package/lib/components/Markdown/variables.js +1 -0
- package/lib/components/Product/utils.js +1 -1
- package/lib/components/Search/SearchAiConversationInput.js +10 -6
- package/lib/components/Search/SearchAiDialog.js +8 -1
- package/lib/components/Search/SearchAiMessage.js +1 -1
- package/lib/components/Search/SearchAiResponse.js +1 -0
- package/lib/components/Search/SearchItem.js +9 -1
- package/lib/core/constants/consent.d.ts +2 -0
- package/lib/core/constants/consent.js +30 -0
- package/lib/core/constants/index.d.ts +1 -0
- package/lib/core/constants/index.js +1 -0
- package/lib/core/constants/search.d.ts +1 -1
- package/lib/core/constants/search.js +1 -1
- package/lib/core/hooks/index.d.ts +2 -0
- package/lib/core/hooks/index.js +2 -0
- package/lib/core/hooks/use-consent-dialog.d.ts +12 -0
- package/lib/core/hooks/use-consent-dialog.js +36 -0
- package/lib/core/hooks/use-consent-telemetry.d.ts +1 -0
- package/lib/core/hooks/use-consent-telemetry.js +42 -0
- package/lib/core/hooks/use-dialog-hotkeys.js +4 -2
- package/lib/core/hooks/use-theme-hooks.js +14 -0
- package/lib/core/styles/global.js +42 -40
- package/lib/core/types/consent.d.ts +54 -0
- package/lib/core/types/consent.js +3 -0
- package/lib/core/types/hooks.d.ts +2 -0
- package/lib/core/types/index.d.ts +1 -0
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/search.d.ts +2 -0
- package/lib/core/utils/consent-settings-link-placement.d.ts +3 -0
- package/lib/core/utils/consent-settings-link-placement.js +15 -0
- package/lib/core/utils/index.d.ts +1 -0
- package/lib/core/utils/index.js +1 -0
- package/lib/core/utils/mcp.js +1 -1
- package/lib/core/utils/menu.js +6 -5
- package/lib/core/utils/urls.js +3 -1
- package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.d.ts +29 -0
- package/lib/icons/ExpandCollapseIcon/ExpandCollapseIcon.js +81 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +3 -3
- package/src/components/CodeBlock/CodeBlockControls.tsx +4 -7
- package/src/components/Consent/Consent.tsx +18 -0
- package/src/components/Consent/ConsentBanner.tsx +110 -0
- package/src/components/Consent/ConsentDialog.tsx +210 -0
- package/src/components/Consent/ConsentSettingsLink.tsx +44 -0
- package/src/components/Consent/variables.ts +28 -0
- package/src/components/Footer/Footer.tsx +2 -0
- package/src/components/JsonViewer/Helpers.tsx +46 -51
- package/src/components/JsonViewer/JsonViewer.tsx +23 -25
- package/src/components/Markdown/styles/base-table.ts +14 -1
- package/src/components/Markdown/variables.ts +1 -0
- package/src/components/Product/utils.ts +1 -1
- package/src/components/Search/SearchAiConversationInput.tsx +13 -7
- package/src/components/Search/SearchAiDialog.tsx +16 -1
- package/src/components/Search/SearchAiMessage.tsx +1 -1
- package/src/components/Search/SearchAiResponse.tsx +1 -0
- package/src/components/Search/SearchItem.tsx +14 -0
- package/src/core/constants/consent.ts +29 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/constants/search.ts +1 -1
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +15 -0
- package/src/core/hooks/code-walkthrough/__mocks__/MockIntersectionObserver.ts +1 -2
- package/src/core/hooks/code-walkthrough/__mocks__/mock-intersection-observer.ts +1 -2
- package/src/core/hooks/index.ts +2 -0
- package/src/core/hooks/use-consent-dialog.ts +51 -0
- package/src/core/hooks/use-consent-telemetry.ts +56 -0
- package/src/core/hooks/use-dialog-hotkeys.ts +11 -5
- package/src/core/hooks/use-theme-hooks.ts +14 -0
- package/src/core/styles/global.ts +2 -0
- package/src/core/types/consent.ts +51 -0
- package/src/core/types/hooks.ts +2 -0
- package/src/core/types/index.ts +1 -0
- package/src/core/types/l10n.ts +25 -1
- package/src/core/types/search.ts +2 -0
- package/src/core/utils/consent-settings-link-placement.ts +18 -0
- package/src/core/utils/index.ts +1 -0
- package/src/core/utils/mcp.ts +1 -1
- package/src/core/utils/menu.ts +5 -6
- package/src/core/utils/urls.ts +3 -1
- package/src/icons/ExpandCollapseIcon/ExpandCollapseIcon.tsx +56 -0
- 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
|
|
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%;
|
|
@@ -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
|
+
];
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 = '';
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -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 =
|
|
25
|
-
|
|
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
|
|
28
|
-
firstFocusableRef.current = focusableElements[0]
|
|
29
|
-
lastFocusableRef.current = focusableElements[focusableElements.length - 1]
|
|
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
|
+
};
|
package/src/core/types/hooks.ts
CHANGED
|
@@ -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,
|
package/src/core/types/index.ts
CHANGED
package/src/core/types/l10n.ts
CHANGED
|
@@ -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
|
|
package/src/core/types/search.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/utils/index.ts
CHANGED
package/src/core/utils/mcp.ts
CHANGED
|
@@ -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(
|
|
42
|
+
.replace(/^-+|(?<!-)-+$/g, '');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function generateClaudeCodeCommand(config: McpConnectionParams): string {
|
package/src/core/utils/menu.ts
CHANGED
|
@@ -24,10 +24,7 @@ export const mapNavbarItems = (
|
|
|
24
24
|
...(navItem.items && {
|
|
25
25
|
items: mapNavbarItems(navItem.items, defaultLocale, currentLocale, locales, location),
|
|
26
26
|
}),
|
|
27
|
-
|
|
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
|
|
46
|
+
? item.link
|
|
47
|
+
: getPathnameForLocale(item.link, defaultLocale, currentLocale, locales);
|
|
49
48
|
|
|
50
49
|
return pathHash === withPathPrefix(link);
|
|
51
50
|
};
|
package/src/core/utils/urls.ts
CHANGED
|
@@ -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
|
|
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';
|