@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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX } from 'react';
|
|
5
|
+
import type { UseConsentResult } from '@redocly/theme/core/types';
|
|
6
|
+
import type { ConsentDraft } from '@redocly/theme/core/hooks';
|
|
7
|
+
|
|
8
|
+
import { Button } from '@redocly/theme/components/Button/Button';
|
|
9
|
+
import { Link } from '@redocly/theme/components/Link/Link';
|
|
10
|
+
import { Portal } from '@redocly/theme/components/Portal/Portal';
|
|
11
|
+
import { Switch } from '@redocly/theme/components/Switch/Switch';
|
|
12
|
+
import { useConsentDialog, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
13
|
+
import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
|
|
14
|
+
|
|
15
|
+
const noop = () => {};
|
|
16
|
+
|
|
17
|
+
export function ConsentDialog(): JSX.Element | null {
|
|
18
|
+
const { useConsent } = useThemeHooks();
|
|
19
|
+
const consent = useConsent();
|
|
20
|
+
|
|
21
|
+
if (!consent.enabled || !consent.isDialogOpen) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return <ConsentDialogContent consent={consent} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ConsentDialogContent({ consent }: { consent: UseConsentResult }): JSX.Element {
|
|
29
|
+
const { useTranslate } = useThemeHooks();
|
|
30
|
+
const { translate } = useTranslate();
|
|
31
|
+
const { draft, setCategory, dialogRef, close, save, categories } = useConsentDialog(consent);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Portal>
|
|
35
|
+
<Overlay
|
|
36
|
+
className="scroll-lock"
|
|
37
|
+
data-component-name="Consent/ConsentDialog"
|
|
38
|
+
onMouseDown={(event) => {
|
|
39
|
+
if (event.target === event.currentTarget) close();
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Dialog
|
|
43
|
+
ref={dialogRef}
|
|
44
|
+
role="dialog"
|
|
45
|
+
aria-modal="true"
|
|
46
|
+
aria-labelledby="redocly-consent-dialog-title"
|
|
47
|
+
tabIndex={-1}
|
|
48
|
+
>
|
|
49
|
+
<TitleRow>
|
|
50
|
+
<Title id="redocly-consent-dialog-title">
|
|
51
|
+
{translate('consent.dialog.title', 'Cookie preferences')}
|
|
52
|
+
</Title>
|
|
53
|
+
<Button
|
|
54
|
+
variant="ghost"
|
|
55
|
+
size="small"
|
|
56
|
+
icon={<CloseIcon />}
|
|
57
|
+
aria-label={translate('consent.dialog.close', 'Close')}
|
|
58
|
+
onClick={close}
|
|
59
|
+
/>
|
|
60
|
+
</TitleRow>
|
|
61
|
+
<Description>
|
|
62
|
+
{translate(
|
|
63
|
+
'consent.dialog.description',
|
|
64
|
+
'Choose which cookies this site may use. You can change your choice at any time.',
|
|
65
|
+
)}{' '}
|
|
66
|
+
{consent.config?.privacyPolicyUrl ? (
|
|
67
|
+
<Link to={consent.config.privacyPolicyUrl} external>
|
|
68
|
+
{translate('consent.privacyPolicy', 'Privacy policy')}
|
|
69
|
+
</Link>
|
|
70
|
+
) : null}
|
|
71
|
+
</Description>
|
|
72
|
+
{categories.map((category) => {
|
|
73
|
+
const gpcLocked = category.id === 'marketing' && consent.gpc;
|
|
74
|
+
return (
|
|
75
|
+
<Row key={category.id}>
|
|
76
|
+
<RowText>
|
|
77
|
+
<RowTitle>
|
|
78
|
+
{translate(category.titleTranslationKey, category.titleDefault)}
|
|
79
|
+
</RowTitle>
|
|
80
|
+
<RowDescription>
|
|
81
|
+
{translate(category.descriptionTranslationKey, category.descriptionDefault)}
|
|
82
|
+
</RowDescription>
|
|
83
|
+
{gpcLocked ? (
|
|
84
|
+
<GpcNotice>
|
|
85
|
+
{translate(
|
|
86
|
+
'consent.gpcNotice',
|
|
87
|
+
'Your browser asked us not to share your data, so this stays off.',
|
|
88
|
+
)}
|
|
89
|
+
</GpcNotice>
|
|
90
|
+
) : null}
|
|
91
|
+
</RowText>
|
|
92
|
+
{category.locked ? (
|
|
93
|
+
<SwitchGroup role="group" aria-label={translate('consent.alwaysOn', 'Always on')}>
|
|
94
|
+
<Switch value={true} disabled onChange={noop} />
|
|
95
|
+
</SwitchGroup>
|
|
96
|
+
) : gpcLocked ? (
|
|
97
|
+
<SwitchGroup
|
|
98
|
+
role="group"
|
|
99
|
+
aria-label={translate('consent.alwaysOff', 'Always off')}
|
|
100
|
+
>
|
|
101
|
+
<Switch value={false} disabled onChange={noop} />
|
|
102
|
+
</SwitchGroup>
|
|
103
|
+
) : (
|
|
104
|
+
<Switch
|
|
105
|
+
value={draft[category.id as keyof ConsentDraft]}
|
|
106
|
+
onChange={(value) => setCategory(category.id as keyof ConsentDraft, value)}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</Row>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
<Actions>
|
|
113
|
+
<Button variant="outlined" onClick={consent.reject}>
|
|
114
|
+
{translate('consent.rejectAll', 'Reject all')}
|
|
115
|
+
</Button>
|
|
116
|
+
<Spacer />
|
|
117
|
+
<Button variant="outlined" onClick={consent.accept}>
|
|
118
|
+
{translate('consent.acceptAll', 'Accept all')}
|
|
119
|
+
</Button>
|
|
120
|
+
<Button variant="primary" onClick={save}>
|
|
121
|
+
{translate('consent.savePreferences', 'Save preferences')}
|
|
122
|
+
</Button>
|
|
123
|
+
</Actions>
|
|
124
|
+
</Dialog>
|
|
125
|
+
</Overlay>
|
|
126
|
+
</Portal>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Overlay = styled.div`
|
|
131
|
+
position: fixed;
|
|
132
|
+
inset: 0;
|
|
133
|
+
z-index: var(--z-index-overlay);
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
background: var(--bg-color-modal-overlay);
|
|
138
|
+
font-family: var(--font-family-base);
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const Dialog = styled.div`
|
|
142
|
+
width: var(--consent-dialog-width);
|
|
143
|
+
max-width: var(--consent-dialog-max-width);
|
|
144
|
+
padding: var(--consent-dialog-padding);
|
|
145
|
+
border-radius: var(--consent-dialog-border-radius);
|
|
146
|
+
background: var(--consent-dialog-bg-color);
|
|
147
|
+
box-shadow: var(--consent-dialog-box-shadow);
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const TitleRow = styled.div`
|
|
151
|
+
display: flex;
|
|
152
|
+
gap: var(--spacing-xs);
|
|
153
|
+
align-items: flex-start;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const Title = styled.h2`
|
|
158
|
+
margin: 0 0 var(--spacing-xs);
|
|
159
|
+
font-size: var(--font-size-lg);
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
const Description = styled.p`
|
|
163
|
+
margin: 0 0 var(--spacing-base);
|
|
164
|
+
color: var(--consent-dialog-description-color);
|
|
165
|
+
font-size: var(--font-size-sm);
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const Row = styled.div`
|
|
169
|
+
display: flex;
|
|
170
|
+
gap: var(--spacing-base);
|
|
171
|
+
align-items: flex-start;
|
|
172
|
+
padding: var(--spacing-sm) 0;
|
|
173
|
+
border-top: 1px solid var(--consent-dialog-row-border-color);
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const RowText = styled.div`
|
|
177
|
+
flex: 1;
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const RowTitle = styled.div`
|
|
181
|
+
font-size: var(--font-size-base);
|
|
182
|
+
font-weight: var(--font-weight-bold);
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const RowDescription = styled.div`
|
|
186
|
+
color: var(--consent-dialog-description-color);
|
|
187
|
+
font-size: var(--font-size-sm);
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const GpcNotice = styled.div`
|
|
191
|
+
color: var(--consent-dialog-description-color);
|
|
192
|
+
font-size: var(--font-size-sm);
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
const SwitchGroup = styled.div`
|
|
196
|
+
flex: none;
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
const Actions = styled.div`
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-wrap: wrap;
|
|
202
|
+
gap: var(--spacing-xs);
|
|
203
|
+
margin-top: var(--spacing-base);
|
|
204
|
+
padding-top: var(--spacing-base);
|
|
205
|
+
border-top: 1px solid var(--consent-dialog-row-border-color);
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const Spacer = styled.span`
|
|
209
|
+
flex: 1;
|
|
210
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@redocly/theme/components/Button/Button';
|
|
7
|
+
import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
|
+
import { resolveSettingsLinkPlacement } from '@redocly/theme/core/utils';
|
|
9
|
+
|
|
10
|
+
export function ConsentSettingsLink(): JSX.Element | null {
|
|
11
|
+
const { useConsent, useTranslate } = useThemeHooks();
|
|
12
|
+
const { consent: consentConfig, footer } = useThemeConfig() || {};
|
|
13
|
+
const consent = useConsent();
|
|
14
|
+
const { translate } = useTranslate();
|
|
15
|
+
|
|
16
|
+
if (!consent.enabled || resolveSettingsLinkPlacement(consentConfig, footer) !== 'footer') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<SettingsLinkButton
|
|
22
|
+
variant="link"
|
|
23
|
+
size="small"
|
|
24
|
+
data-component-name="Consent/ConsentSettingsLink"
|
|
25
|
+
onClick={consent.openDialog}
|
|
26
|
+
>
|
|
27
|
+
{translate('consent.settingsLink', 'Cookie preferences')}
|
|
28
|
+
</SettingsLinkButton>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SettingsLinkButton = styled(Button)`
|
|
33
|
+
margin: var(--footer-link-padding-vertical) 0 var(--footer-link-padding-vertical) var(--spacing-sm);
|
|
34
|
+
padding: 0;
|
|
35
|
+
color: var(--footer-link-text-color);
|
|
36
|
+
font: inherit;
|
|
37
|
+
line-height: inherit;
|
|
38
|
+
|
|
39
|
+
&:hover,
|
|
40
|
+
&:focus-visible {
|
|
41
|
+
color: var(--footer-link-color-hover);
|
|
42
|
+
background: none;
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const consent = css`
|
|
4
|
+
/**
|
|
5
|
+
* @tokens Consent
|
|
6
|
+
*/
|
|
7
|
+
--consent-banner-width: 380px;
|
|
8
|
+
--consent-banner-offset: var(--spacing-base); // @presenter Spacing
|
|
9
|
+
--consent-banner-bg-color: var(--bg-color); // @presenter Color
|
|
10
|
+
--consent-banner-text-color: var(--text-color-primary); // @presenter Color
|
|
11
|
+
--consent-banner-border-color: var(--border-color-primary); // @presenter Color
|
|
12
|
+
--consent-banner-border-radius: var(--border-radius-lg);
|
|
13
|
+
--consent-banner-box-shadow: var(--bg-raised-shadow);
|
|
14
|
+
--consent-banner-padding: var(--spacing-base); // @presenter Spacing
|
|
15
|
+
--consent-banner-gap: var(--spacing-sm); // @presenter Spacing
|
|
16
|
+
--consent-banner-title-font-size: var(--font-size-base);
|
|
17
|
+
--consent-banner-font-size: var(--font-size-sm);
|
|
18
|
+
--consent-banner-z-index: var(--z-index-overlay);
|
|
19
|
+
|
|
20
|
+
--consent-dialog-width: 560px;
|
|
21
|
+
--consent-dialog-max-width: calc(100vw - 2 * var(--spacing-base));
|
|
22
|
+
--consent-dialog-padding: var(--spacing-lg); // @presenter Spacing
|
|
23
|
+
--consent-dialog-border-radius: var(--border-radius-lg);
|
|
24
|
+
--consent-dialog-bg-color: var(--modal-bg-color); // @presenter Color
|
|
25
|
+
--consent-dialog-box-shadow: var(--modal-box-shadow);
|
|
26
|
+
--consent-dialog-row-border-color: var(--border-color-primary); // @presenter Color
|
|
27
|
+
--consent-dialog-description-color: var(--text-color-secondary); // @presenter Color
|
|
28
|
+
`;
|
|
@@ -9,6 +9,7 @@ import { breakpoints, isEmptyArray } from '@redocly/theme/core/utils';
|
|
|
9
9
|
import { useThemeConfig } from '@redocly/theme/core/hooks';
|
|
10
10
|
import { FooterCopyright } from '@redocly/theme/components/Footer/FooterCopyright';
|
|
11
11
|
import { FooterColumn } from '@redocly/theme/components/Footer/FooterColumn';
|
|
12
|
+
import { ConsentSettingsLink } from '@redocly/theme/components/Consent/ConsentSettingsLink';
|
|
12
13
|
|
|
13
14
|
export type FooterProps = {
|
|
14
15
|
className?: string;
|
|
@@ -43,6 +44,7 @@ export function Footer({ className }: FooterProps): JSX.Element | null {
|
|
|
43
44
|
)}
|
|
44
45
|
<FooterCopyrightWrapper>
|
|
45
46
|
{copyrightText && <FooterCopyright copyrightText={copyrightText} />}
|
|
47
|
+
<ConsentSettingsLink />
|
|
46
48
|
</FooterCopyrightWrapper>
|
|
47
49
|
</FooterWrapper>
|
|
48
50
|
);
|
|
@@ -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
|
|
@@ -30,18 +30,24 @@ export function SearchAiConversationInput({
|
|
|
30
30
|
const { translate } = useTranslate();
|
|
31
31
|
|
|
32
32
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
33
|
+
const wasGeneratingResponseRef = useRef(isGeneratingResponse);
|
|
33
34
|
const [query, setQuery] = useState('');
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
+
const wasGeneratingResponse = wasGeneratingResponseRef.current;
|
|
38
|
+
wasGeneratingResponseRef.current = isGeneratingResponse;
|
|
39
|
+
|
|
40
|
+
if (wasGeneratingResponse && !isGeneratingResponse) {
|
|
37
41
|
requestAnimationFrame(() => {
|
|
38
42
|
inputRef.current?.focus();
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
}, [isGeneratingResponse]);
|
|
42
46
|
|
|
47
|
+
const isDisabled = disabled || isGeneratingResponse || query.trim().length === 0;
|
|
48
|
+
|
|
43
49
|
const handleSendMessage = () => {
|
|
44
|
-
if (
|
|
50
|
+
if (isDisabled) return;
|
|
45
51
|
|
|
46
52
|
setQuery('');
|
|
47
53
|
onMessageSent(query);
|
|
@@ -59,8 +65,6 @@ export function SearchAiConversationInput({
|
|
|
59
65
|
}
|
|
60
66
|
};
|
|
61
67
|
|
|
62
|
-
const isDisabled = disabled || isGeneratingResponse || query.trim().length === 0;
|
|
63
|
-
|
|
64
68
|
const commonProps = {
|
|
65
69
|
placeholder:
|
|
66
70
|
placeholder || translate('search.ai.followUpQuestion', 'Ask a follow up question?'),
|
|
@@ -91,7 +95,8 @@ export function SearchAiConversationInput({
|
|
|
91
95
|
)}
|
|
92
96
|
|
|
93
97
|
<SendButton
|
|
94
|
-
|
|
98
|
+
aria-label={translate('search.ai.send', 'Send question')}
|
|
99
|
+
aria-disabled={isDisabled}
|
|
95
100
|
size="small"
|
|
96
101
|
icon={
|
|
97
102
|
<SendIcon
|
|
@@ -176,11 +181,12 @@ const SendButton = styled(Button)`
|
|
|
176
181
|
border-radius: var(--search-ai-conversation-input-send-button-border-radius);
|
|
177
182
|
padding: var(--search-ai-conversation-input-send-button-padding);
|
|
178
183
|
|
|
179
|
-
&:hover {
|
|
184
|
+
&:hover:not([aria-disabled='true']) {
|
|
180
185
|
background-color: var(--search-ai-conversation-input-send-button-bg-color-hover);
|
|
181
186
|
}
|
|
182
187
|
|
|
183
|
-
|
|
188
|
+
&[aria-disabled='true'] {
|
|
189
|
+
cursor: default;
|
|
184
190
|
background-color: var(--search-ai-conversation-input-send-button-bg-color-disabled);
|
|
185
191
|
border: var(--search-ai-conversation-input-send-button-border-disabled);
|
|
186
192
|
}
|