@redocly/theme 0.70.0-next.2 → 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/SearchAiMessage.js +1 -1
- package/lib/components/Search/SearchAiResponse.js +1 -0
- 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/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-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/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/SearchAiMessage.tsx +1 -1
- package/src/components/Search/SearchAiResponse.tsx +1 -0
- package/src/core/constants/consent.ts +29 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +15 -0
- 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-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 +22 -1
- 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
|
@@ -7,40 +7,50 @@ import { Button } from '@redocly/theme/components/Button/Button';
|
|
|
7
7
|
import { ShowMoreToggleButton } from '@redocly/theme/components/Button/ShowMoreToggleButton';
|
|
8
8
|
import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
|
|
9
9
|
|
|
10
|
+
export type JsonFold = {
|
|
11
|
+
isExpanded: (path: string, level: number) => boolean;
|
|
12
|
+
setExpanded: (path: string, expanded: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Foldable = [path: string, level: number];
|
|
16
|
+
|
|
17
|
+
export function isFoldable(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return (
|
|
19
|
+
typeof value === 'object' &&
|
|
20
|
+
value !== null &&
|
|
21
|
+
Object.values(value).some((item) => item !== undefined)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function collectFoldables(value: unknown, path = '', level = 1): Foldable[] {
|
|
26
|
+
if (!isFoldable(value)) return [];
|
|
27
|
+
return Object.entries(value).flatMap(([key, child]): Foldable[] => {
|
|
28
|
+
const childPath = `${path}/${key}`;
|
|
29
|
+
return isFoldable(child)
|
|
30
|
+
? [[childPath, level], ...collectFoldables(child, childPath, level + 1)]
|
|
31
|
+
: [];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
export function JsonValue(props: {
|
|
11
36
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
12
37
|
value: any;
|
|
13
38
|
level: number;
|
|
39
|
+
path: string;
|
|
14
40
|
standalone?: boolean; // if true, will render the opening and closing punctuation (when it's an item inside array or top-level)
|
|
15
|
-
|
|
16
|
-
expandAllSignal: boolean | undefined;
|
|
41
|
+
fold: JsonFold;
|
|
17
42
|
maxStringLength?: number;
|
|
18
43
|
}): React.ReactNode {
|
|
19
|
-
const { standalone,
|
|
44
|
+
const { standalone, fold, level, path, value, maxStringLength } = props;
|
|
20
45
|
const indent = ' '.repeat(level * 2);
|
|
21
|
-
|
|
22
|
-
const [isExpandedState, setExpanded] = React.useState(
|
|
23
|
-
expandAllSignal ?? level < defaultExpandLevel,
|
|
24
|
-
);
|
|
25
|
-
const isExpanded = expandAllSignal !== undefined && level > 0 ? expandAllSignal : isExpandedState;
|
|
26
|
-
const [prevExpandAllSignal, setPrevExpandAllSignal] = React.useState(expandAllSignal);
|
|
27
|
-
const [prevLevel, setPrevLevel] = React.useState(level);
|
|
28
|
-
|
|
29
|
-
if (prevExpandAllSignal !== expandAllSignal || prevLevel !== level) {
|
|
30
|
-
setPrevExpandAllSignal(expandAllSignal);
|
|
31
|
-
setPrevLevel(level);
|
|
32
|
-
if (expandAllSignal !== undefined && !(expandAllSignal === false && level === 0)) {
|
|
33
|
-
setExpanded(expandAllSignal);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
46
|
+
const isExpanded = fold.isExpanded(path, level);
|
|
36
47
|
|
|
37
48
|
const keys: string[] =
|
|
38
49
|
(value && Object.keys(value).filter((key) => value[key] !== undefined)) || []; // filter out undefined properties
|
|
39
50
|
|
|
40
51
|
const valueType = typeof value;
|
|
41
|
-
const isPrimitive = (valueType !== 'object' && value !== null) || keys.length === 0;
|
|
42
52
|
|
|
43
|
-
if (
|
|
53
|
+
if (!isFoldable(value)) {
|
|
44
54
|
const isOversizedString =
|
|
45
55
|
typeof value === 'string' && maxStringLength !== undefined && value.length > maxStringLength;
|
|
46
56
|
return (
|
|
@@ -64,9 +74,9 @@ export function JsonValue(props: {
|
|
|
64
74
|
<JsonValue
|
|
65
75
|
value={item}
|
|
66
76
|
level={level + 1}
|
|
77
|
+
path={`${path}/${index}`}
|
|
67
78
|
standalone={true}
|
|
68
|
-
|
|
69
|
-
expandAllSignal={expandAllSignal}
|
|
79
|
+
fold={fold}
|
|
70
80
|
maxStringLength={maxStringLength}
|
|
71
81
|
/>
|
|
72
82
|
{index < value.length - 1 ? ',\n' : null}
|
|
@@ -78,8 +88,8 @@ export function JsonValue(props: {
|
|
|
78
88
|
name={key}
|
|
79
89
|
value={value[key]}
|
|
80
90
|
level={level + 1}
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
path={`${path}/${key}`}
|
|
92
|
+
fold={fold}
|
|
83
93
|
maxStringLength={maxStringLength}
|
|
84
94
|
/>
|
|
85
95
|
{index < keys.length - 1 ? ',\n' : null}
|
|
@@ -90,7 +100,7 @@ export function JsonValue(props: {
|
|
|
90
100
|
<>
|
|
91
101
|
{indent}
|
|
92
102
|
{level > 0 ? (
|
|
93
|
-
<ExpandIcon onClick={() => setExpanded(
|
|
103
|
+
<ExpandIcon onClick={() => fold.setExpanded(path, !isExpanded)} isExpanded={isExpanded} />
|
|
94
104
|
) : null}
|
|
95
105
|
{openingPunctuation}
|
|
96
106
|
{isExpanded ? '\n' : null}
|
|
@@ -119,33 +129,18 @@ function JsonKeyValue(props: {
|
|
|
119
129
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
120
130
|
value: any;
|
|
121
131
|
level: number;
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
path: string;
|
|
133
|
+
fold: JsonFold;
|
|
124
134
|
maxStringLength?: number;
|
|
125
135
|
}): React.ReactNode {
|
|
126
|
-
const {
|
|
136
|
+
const { fold, level, name, path, value, maxStringLength } = props;
|
|
127
137
|
const indent = ' '.repeat(level * 2);
|
|
128
|
-
const
|
|
129
|
-
(value && Object.keys(value).filter((key) => value[key] !== undefined)) || []; // filter out undefined properties
|
|
130
|
-
|
|
131
|
-
const isCollapsible = typeof value === 'object' && value !== null && keys.length > 0;
|
|
132
|
-
const [isExpandedState, setExpanded] = React.useState(
|
|
133
|
-
expandAllSignal ?? level < defaultExpandLevel,
|
|
134
|
-
);
|
|
135
|
-
const isExpanded = expandAllSignal !== undefined ? expandAllSignal : isExpandedState;
|
|
136
|
-
const [prevExpandAllSignal, setPrevExpandAllSignal] = React.useState(expandAllSignal);
|
|
137
|
-
|
|
138
|
-
if (prevExpandAllSignal !== expandAllSignal) {
|
|
139
|
-
setPrevExpandAllSignal(expandAllSignal);
|
|
140
|
-
if (expandAllSignal !== undefined) {
|
|
141
|
-
setExpanded(expandAllSignal);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
138
|
+
const isExpanded = fold.isExpanded(path, level);
|
|
144
139
|
|
|
145
140
|
const openingPunctuation = Array.isArray(value) ? '[' : '{';
|
|
146
141
|
const closingPunctuation = Array.isArray(value) ? ']' : '}';
|
|
147
142
|
|
|
148
|
-
const valueElement =
|
|
143
|
+
const valueElement = isFoldable(value) ? (
|
|
149
144
|
<>
|
|
150
145
|
{openingPunctuation}
|
|
151
146
|
{isExpanded ? (
|
|
@@ -154,8 +149,8 @@ function JsonKeyValue(props: {
|
|
|
154
149
|
<JsonValue
|
|
155
150
|
value={value}
|
|
156
151
|
level={level}
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
path={path}
|
|
153
|
+
fold={fold}
|
|
159
154
|
maxStringLength={maxStringLength}
|
|
160
155
|
/>
|
|
161
156
|
{'\n'}
|
|
@@ -170,8 +165,8 @@ function JsonKeyValue(props: {
|
|
|
170
165
|
<JsonValue
|
|
171
166
|
value={value}
|
|
172
167
|
level={level}
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
path={path}
|
|
169
|
+
fold={fold}
|
|
175
170
|
maxStringLength={maxStringLength}
|
|
176
171
|
/>
|
|
177
172
|
);
|
|
@@ -179,8 +174,8 @@ function JsonKeyValue(props: {
|
|
|
179
174
|
return (
|
|
180
175
|
<>
|
|
181
176
|
{indent}
|
|
182
|
-
{
|
|
183
|
-
<ExpandIcon onClick={() => setExpanded(
|
|
177
|
+
{isFoldable(value) ? (
|
|
178
|
+
<ExpandIcon onClick={() => fold.setExpanded(path, !isExpanded)} isExpanded={isExpanded} />
|
|
184
179
|
) : null}
|
|
185
180
|
<span className="property token string">{`"${name}"`}</span>: {valueElement}
|
|
186
181
|
</>
|
|
@@ -5,7 +5,8 @@ import type { JSX } from 'react';
|
|
|
5
5
|
import type { CodeBlockControlsProps } from '@redocly/theme/components/CodeBlock/CodeBlockControls';
|
|
6
6
|
|
|
7
7
|
import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
8
|
-
import { JsonValue } from '@redocly/theme/components/JsonViewer/Helpers';
|
|
8
|
+
import { collectFoldables, JsonValue } from '@redocly/theme/components/JsonViewer/Helpers';
|
|
9
|
+
import type { JsonFold } from '@redocly/theme/components/JsonViewer/Helpers';
|
|
9
10
|
|
|
10
11
|
export type PanelType = 'request' | 'responses' | 'request-samples' | 'response-samples';
|
|
11
12
|
|
|
@@ -33,25 +34,20 @@ function JsonComponent({
|
|
|
33
34
|
controls = {},
|
|
34
35
|
maxStringLength,
|
|
35
36
|
}: JsonProps): JSX.Element {
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const defaultExpandLevel = Math.max(1, expandLevel);
|
|
38
|
+
const foldables = React.useMemo(() => collectFoldables(data), [data]);
|
|
39
|
+
const [expandedPaths, setExpandedPaths] = React.useState<Record<string, boolean>>({});
|
|
38
40
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
setExpandAllSignal(true);
|
|
43
|
-
onPanelToggle?.(true);
|
|
44
|
-
setTimeout(() => {
|
|
45
|
-
setExpandAllSignal(undefined);
|
|
46
|
-
});
|
|
41
|
+
const fold: JsonFold = {
|
|
42
|
+
isExpanded: (path, level) => expandedPaths[path] ?? level < defaultExpandLevel,
|
|
43
|
+
setExpanded: (path, expanded) => setExpandedPaths((prev) => ({ ...prev, [path]: expanded })),
|
|
47
44
|
};
|
|
45
|
+
const hasFoldables = foldables.length > 0;
|
|
46
|
+
const allExpanded = foldables.every(([path, level]) => fold.isExpanded(path, level));
|
|
48
47
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
setExpandAllSignal(undefined);
|
|
54
|
-
});
|
|
48
|
+
const toggleAll = () => {
|
|
49
|
+
onPanelToggle?.(!allExpanded);
|
|
50
|
+
setExpandedPaths(Object.fromEntries(foldables.map(([path]) => [path, !allExpanded])));
|
|
55
51
|
};
|
|
56
52
|
|
|
57
53
|
const source = JSON.stringify(data, null, 2);
|
|
@@ -73,12 +69,14 @@ function JsonComponent({
|
|
|
73
69
|
controls: controls && {
|
|
74
70
|
...controls,
|
|
75
71
|
copy: { ...controls.copy, data, onClick: onCopyClick, handleOutside: true },
|
|
76
|
-
expand:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
expand:
|
|
73
|
+
hasFoldables && !allExpanded
|
|
74
|
+
? { ...controls.expand, onClick: toggleAll }
|
|
75
|
+
: undefined,
|
|
76
|
+
collapse:
|
|
77
|
+
hasFoldables && allExpanded
|
|
78
|
+
? { ...controls.collapse, onClick: toggleAll }
|
|
79
|
+
: undefined,
|
|
82
80
|
},
|
|
83
81
|
}
|
|
84
82
|
: undefined
|
|
@@ -89,9 +87,9 @@ function JsonComponent({
|
|
|
89
87
|
<JsonValue
|
|
90
88
|
value={data}
|
|
91
89
|
level={0}
|
|
90
|
+
path=""
|
|
92
91
|
standalone={true}
|
|
93
|
-
|
|
94
|
-
defaultExpandLevel={Math.max(1, expandLevel)}
|
|
92
|
+
fold={fold}
|
|
95
93
|
maxStringLength={maxStringLength}
|
|
96
94
|
/>
|
|
97
95
|
</FoldingWrap>
|
|
@@ -22,7 +22,7 @@ export const markdownBaseTableCss = css`
|
|
|
22
22
|
color: var(--md-table-cell-text-color);
|
|
23
23
|
font-size: var(--md-table-font-size);
|
|
24
24
|
line-height: var(--md-table-line-height);
|
|
25
|
-
vertical-align:
|
|
25
|
+
vertical-align: var(--md-table-cell-vertical-align);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
thead td,
|
|
@@ -71,5 +71,18 @@ export const markdownBaseTableCss = css`
|
|
|
71
71
|
th[align='right'] {
|
|
72
72
|
text-align: right;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
th[valign='top'],
|
|
76
|
+
td[valign='top'] {
|
|
77
|
+
vertical-align: top;
|
|
78
|
+
}
|
|
79
|
+
th[valign='middle'],
|
|
80
|
+
td[valign='middle'] {
|
|
81
|
+
vertical-align: middle;
|
|
82
|
+
}
|
|
83
|
+
th[valign='bottom'],
|
|
84
|
+
td[valign='bottom'] {
|
|
85
|
+
vertical-align: bottom;
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
`;
|
|
@@ -51,6 +51,7 @@ export const markdown = css`
|
|
|
51
51
|
--md-table-cell-text-color: var(--text-color-secondary); // @presenter Color
|
|
52
52
|
--md-table-cell-padding: var(--spacing-xs) var(--spacing-sm); // @presenter Spacing
|
|
53
53
|
--md-table-footer-cell-padding: var(--spacing-xxs) var(--spacing-sm); // @presenter Spacing
|
|
54
|
+
--md-table-cell-vertical-align: top;
|
|
54
55
|
|
|
55
56
|
--md-table-head-bg-color: var(--md-table-bg-color); // @presenter Color
|
|
56
57
|
--md-table-head-text-color: var(--text-color-primary); // @presenter Color
|
|
@@ -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%;
|
|
@@ -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
|
+
];
|
|
@@ -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: [],
|
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
|
+
}
|
|
@@ -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
|
@@ -513,7 +513,28 @@ export type TranslationKey =
|
|
|
513
513
|
| 'diagram.zoomOut'
|
|
514
514
|
| 'diagram.reset'
|
|
515
515
|
| 'diagram.close'
|
|
516
|
-
| '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';
|
|
517
538
|
|
|
518
539
|
export type Locale = { code: string; name: string };
|
|
519
540
|
|
|
@@ -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
|
+
}
|