@redocly/theme 0.69.0-next.8 → 0.69.0
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/Accordion/Accordion.js +5 -1
- package/lib/components/Catalog/CatalogEntities.js +1 -0
- package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.js +5 -3
- package/lib/components/Catalog/CatalogViewModeToggle.d.ts +1 -1
- package/lib/components/Catalog/CatalogViewModeToggle.js +2 -43
- package/lib/components/Filter/FilterInput.js +4 -2
- package/lib/components/JsonViewer/Helpers.js +12 -42
- package/lib/components/MCP/McpConnectionTabs.js +21 -11
- package/lib/components/Markdown/Markdown.js +1 -3
- package/lib/components/Markdown/styles/base-table.js +8 -0
- package/lib/components/Markdown/variables.js +1 -2
- package/lib/components/Panel/Panel.js +5 -1
- package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +22 -32
- package/lib/components/Search/SearchAiDialog.js +3 -0
- package/lib/components/Search/SearchItem.js +5 -3
- package/lib/components/Search/variables.js +1 -0
- package/lib/components/Select/Select.js +10 -5
- package/lib/components/Sidebar/Sidebar.js +2 -38
- package/lib/core/contexts/AccordionGroupContext.d.ts +1 -0
- package/lib/core/contexts/AccordionGroupContext.js +6 -0
- package/lib/core/contexts/PageActionsContext.d.ts +5 -0
- package/lib/core/contexts/PageActionsContext.js +10 -0
- package/lib/core/contexts/index.d.ts +2 -0
- package/lib/core/contexts/index.js +2 -0
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough-controls.js +5 -17
- package/lib/core/hooks/code-walkthrough/use-code-walkthrough-steps.js +2 -0
- package/lib/core/hooks/menu/use-menu-item-expanded.js +1 -0
- package/lib/core/hooks/use-active-heading.js +4 -0
- package/lib/core/hooks/use-connect-mcp-button.js +7 -2
- package/lib/core/hooks/use-local-state.js +1 -0
- package/lib/core/hooks/use-mobile-menu.js +10 -2
- package/lib/core/hooks/use-product-picker.js +1 -1
- package/lib/core/hooks/use-tabs.js +4 -2
- package/lib/core/hooks/use-telemetry-fallback.d.ts +2 -0
- package/lib/core/hooks/use-telemetry-fallback.js +2 -0
- package/lib/core/styles/global.js +1 -0
- package/lib/core/utils/class-names.d.ts +1 -0
- package/lib/core/utils/class-names.js +7 -0
- package/lib/markdoc/components/Accordion/Accordion.js +6 -6
- package/lib/markdoc/components/Accordion/AccordionGroup.d.ts +2 -1
- package/lib/markdoc/components/Accordion/AccordionGroup.js +39 -3
- package/lib/markdoc/components/Heading/Heading.js +5 -2
- package/lib/markdoc/tags/accordion-group.js +3 -0
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.tsx +7 -3
- package/src/components/Catalog/CatalogEntities.tsx +1 -0
- package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelations.tsx +6 -4
- package/src/components/Catalog/CatalogViewModeToggle.tsx +2 -17
- package/src/components/Filter/FilterInput.tsx +5 -3
- package/src/components/JsonViewer/Helpers.tsx +13 -9
- package/src/components/MCP/McpConnectionTabs.tsx +32 -10
- package/src/components/Markdown/Markdown.tsx +1 -3
- package/src/components/Markdown/styles/base-table.ts +8 -0
- package/src/components/Markdown/variables.ts +1 -2
- package/src/components/Panel/Panel.tsx +7 -3
- package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +32 -40
- package/src/components/Search/SearchAiDialog.tsx +3 -0
- package/src/components/Search/SearchItem.tsx +6 -1
- package/src/components/Search/variables.ts +1 -0
- package/src/components/Select/Select.tsx +12 -6
- package/src/components/Sidebar/Sidebar.tsx +2 -6
- package/src/core/contexts/AccordionGroupContext.tsx +3 -0
- package/src/core/contexts/PageActionsContext.tsx +7 -0
- package/src/core/contexts/index.ts +2 -0
- package/src/core/hooks/__mocks__/use-theme-hooks.ts +2 -0
- package/src/core/hooks/code-walkthrough/use-code-walkthrough-controls.ts +6 -20
- package/src/core/hooks/code-walkthrough/use-code-walkthrough-steps.ts +2 -0
- package/src/core/hooks/menu/use-menu-item-expanded.ts +1 -0
- package/src/core/hooks/use-active-heading.ts +4 -0
- package/src/core/hooks/use-connect-mcp-button.ts +8 -2
- package/src/core/hooks/use-local-state.ts +1 -0
- package/src/core/hooks/use-mobile-menu.ts +11 -3
- package/src/core/hooks/use-product-picker.ts +2 -2
- package/src/core/hooks/use-tabs.ts +4 -2
- package/src/core/hooks/use-telemetry-fallback.ts +2 -0
- package/src/core/styles/global.ts +1 -0
- package/src/core/utils/class-names.ts +8 -0
- package/src/markdoc/components/Accordion/Accordion.tsx +8 -2
- package/src/markdoc/components/Accordion/AccordionGroup.tsx +9 -2
- package/src/markdoc/components/Heading/Heading.tsx +7 -4
- package/src/markdoc/tags/accordion-group.ts +3 -0
|
@@ -3,6 +3,8 @@ import styled from 'styled-components';
|
|
|
3
3
|
|
|
4
4
|
import type { JSX, ReactNode } from 'react';
|
|
5
5
|
import type { TFunction } from '@redocly/theme/core/types';
|
|
6
|
+
import type { AsyncApiRealmUI } from '@redocly/realm-asyncapi-sdk';
|
|
7
|
+
import type { CodeBlockControlsProps } from '@redocly/theme/components/CodeBlock/CodeBlockControls';
|
|
6
8
|
|
|
7
9
|
import { useMCPConfig, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
10
|
import {
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
generateClaudeDesktopConfig,
|
|
11
13
|
generateCodexCommand,
|
|
12
14
|
generateMcpInstallPrompt,
|
|
15
|
+
getBaseDataAttributes,
|
|
13
16
|
} from '@redocly/theme/core/utils';
|
|
14
17
|
import { CodeBlock } from '@redocly/theme/components/CodeBlock/CodeBlock';
|
|
15
18
|
import { ClaudeIcon } from '@redocly/theme/icons/ClaudeIcon/ClaudeIcon';
|
|
@@ -47,15 +50,18 @@ const TAB_LABELS: Record<
|
|
|
47
50
|
|
|
48
51
|
type TabClient = { headline: string; content: ReactNode };
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
type SnippetId =
|
|
54
|
+
AsyncApiRealmUI.EventPayload<'com.redocly.mcpConnectionSnippet.copied'>[0]['snippet'];
|
|
55
|
+
|
|
56
|
+
type SnippetHeader = (client: McpConnectionTab, snippet: SnippetId) => CodeBlockControlsProps;
|
|
52
57
|
|
|
53
58
|
export function McpConnectionTabs({
|
|
54
59
|
options = DEFAULT_TAB_OPTIONS,
|
|
55
60
|
className,
|
|
56
61
|
}: McpConnectionTabsProps): JSX.Element | null {
|
|
57
|
-
const { useTranslate } = useThemeHooks();
|
|
62
|
+
const { useTranslate, useTelemetry } = useThemeHooks();
|
|
58
63
|
const { translate } = useTranslate();
|
|
64
|
+
const telemetry = useTelemetry();
|
|
59
65
|
const { serverName, serverUrl, isMcpDisabled } = useMCPConfig();
|
|
60
66
|
// The SSR markup of error pages is cached with the build-time origin, so the
|
|
61
67
|
// tabs remount after hydration to pick up the real browser origin.
|
|
@@ -69,6 +75,19 @@ export function McpConnectionTabs({
|
|
|
69
75
|
|
|
70
76
|
const connection = { serverName, url: serverUrl };
|
|
71
77
|
|
|
78
|
+
const snippetHeader: SnippetHeader = (client, snippet) => ({
|
|
79
|
+
controls: {
|
|
80
|
+
copy: {
|
|
81
|
+
onClick: () =>
|
|
82
|
+
telemetry.sendMcpConnectionSnippetCopiedMessage([
|
|
83
|
+
{ ...getBaseDataAttributes('mcpConnectionSnippet', 'button'), client, snippet },
|
|
84
|
+
]),
|
|
85
|
+
},
|
|
86
|
+
// The report control is docs feedback, not useful on the connection page.
|
|
87
|
+
report: { hidden: true },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
72
91
|
return (
|
|
73
92
|
<McpConnectionTabsWrapper
|
|
74
93
|
key={hydrated ? 'client' : 'ssr'}
|
|
@@ -86,7 +105,7 @@ export function McpConnectionTabs({
|
|
|
86
105
|
label={translate(translationKey, defaultLabel)}
|
|
87
106
|
icon={TabIcon ? <TabIcon /> : undefined}
|
|
88
107
|
>
|
|
89
|
-
{renderTabContent(option, connection, translate)}
|
|
108
|
+
{renderTabContent(option, connection, translate, snippetHeader)}
|
|
90
109
|
</TabItemFragment>
|
|
91
110
|
);
|
|
92
111
|
})}
|
|
@@ -99,6 +118,7 @@ function renderTabContent(
|
|
|
99
118
|
option: McpConnectionTab,
|
|
100
119
|
connection: { serverName: string; url: string },
|
|
101
120
|
translate: TFunction,
|
|
121
|
+
snippetHeader: SnippetHeader,
|
|
102
122
|
): ReactNode {
|
|
103
123
|
const terminalHint = (
|
|
104
124
|
<TabHint>
|
|
@@ -106,7 +126,9 @@ function renderTabContent(
|
|
|
106
126
|
</TabHint>
|
|
107
127
|
);
|
|
108
128
|
|
|
109
|
-
const urlBlock =
|
|
129
|
+
const urlBlock = (
|
|
130
|
+
<CodeBlock lang="text" source={connection.url} header={snippetHeader(option, 'chatgpt-app')} />
|
|
131
|
+
);
|
|
110
132
|
|
|
111
133
|
const assistantPromptClient: TabClient = {
|
|
112
134
|
headline: translate('page.mcpNotAllowed.snippets.prompt.headline', 'Prompt'),
|
|
@@ -121,7 +143,7 @@ function renderTabContent(
|
|
|
121
143
|
<CodeBlock
|
|
122
144
|
lang="text"
|
|
123
145
|
source={generateMcpInstallPrompt(connection)}
|
|
124
|
-
header={
|
|
146
|
+
header={snippetHeader(option, 'prompt')}
|
|
125
147
|
wrapContents
|
|
126
148
|
/>
|
|
127
149
|
</>
|
|
@@ -139,7 +161,7 @@ function renderTabContent(
|
|
|
139
161
|
<CodeBlock
|
|
140
162
|
lang="shell"
|
|
141
163
|
source={generateClaudeCodeCommand(connection)}
|
|
142
|
-
header={
|
|
164
|
+
header={snippetHeader(option, 'claude-code')}
|
|
143
165
|
wrapContents
|
|
144
166
|
/>
|
|
145
167
|
</>
|
|
@@ -161,7 +183,7 @@ function renderTabContent(
|
|
|
161
183
|
<CodeBlock
|
|
162
184
|
lang="json"
|
|
163
185
|
source={generateClaudeDesktopConfig(connection)}
|
|
164
|
-
header={
|
|
186
|
+
header={snippetHeader(option, 'claude-desktop')}
|
|
165
187
|
/>
|
|
166
188
|
</>
|
|
167
189
|
),
|
|
@@ -177,7 +199,7 @@ function renderTabContent(
|
|
|
177
199
|
<CodeBlock
|
|
178
200
|
lang="shell"
|
|
179
201
|
source={generateCodexCommand(connection)}
|
|
180
|
-
header={
|
|
202
|
+
header={snippetHeader(option, 'codex')}
|
|
181
203
|
wrapContents
|
|
182
204
|
/>
|
|
183
205
|
</>
|
|
@@ -218,7 +240,7 @@ function renderTabContent(
|
|
|
218
240
|
null,
|
|
219
241
|
2,
|
|
220
242
|
)}
|
|
221
|
-
header={
|
|
243
|
+
header={snippetHeader(option, 'json')}
|
|
222
244
|
/>
|
|
223
245
|
</>
|
|
224
246
|
);
|
|
@@ -167,10 +167,8 @@ export const Markdown = styled.main
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
hr {
|
|
170
|
-
border-top: 1px solid var(--md-hr-border-color);
|
|
171
|
-
background-color: var(--md-hr-bg-color);
|
|
172
|
-
height: var(--md-hr-height);
|
|
173
170
|
margin: var(--md-hr-margin-vertical) 0;
|
|
171
|
+
border: var(--md-hr-border);
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
details:not(${ACCORDION_SELECTOR}) {
|
|
@@ -45,6 +45,14 @@ export const markdownBaseTableCss = css`
|
|
|
45
45
|
background-color: var(--md-table-header-bg-color);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/* hide the header row when every cell is empty; one labelled cell keeps it visible */
|
|
49
|
+
thead:has(th:empty) {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
thead:has(th:not(:empty)) {
|
|
53
|
+
display: table-header-group;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
tr {
|
|
49
57
|
background-color: var(--md-table-stripe-bg-color);
|
|
50
58
|
}
|
|
@@ -60,9 +60,8 @@ export const markdown = css`
|
|
|
60
60
|
* @tokens Markdown Horizontal Rule
|
|
61
61
|
*/
|
|
62
62
|
|
|
63
|
-
--md-hr-bg-color: transparent; // @presenter Color
|
|
64
63
|
--md-hr-border-color: var(--border-color-secondary); // @presenter Color
|
|
65
|
-
--md-hr-
|
|
64
|
+
--md-hr-border: 0.5px solid var(--md-hr-border-color); // @presenter Border
|
|
66
65
|
--md-hr-margin-vertical: 20px; // @presenter Spacing
|
|
67
66
|
|
|
68
67
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { BaseSyntheticEvent, PropsWithChildren, ReactNode, JSX } from 'react';
|
|
@@ -34,6 +34,12 @@ export function Panel({
|
|
|
34
34
|
}: PropsWithChildren<PanelProps>): JSX.Element {
|
|
35
35
|
const [isExpanded, setExpanded] = useState(expanded);
|
|
36
36
|
const [animate, setAnimate] = useState(false);
|
|
37
|
+
const [prevExpanded, setPrevExpanded] = useState(expanded);
|
|
38
|
+
|
|
39
|
+
if (prevExpanded !== expanded) {
|
|
40
|
+
setPrevExpanded(expanded);
|
|
41
|
+
setExpanded(expanded);
|
|
42
|
+
}
|
|
37
43
|
|
|
38
44
|
const toggle = ({ target }: BaseSyntheticEvent): void => {
|
|
39
45
|
if (target instanceof HTMLAnchorElement || !isExpandable) return;
|
|
@@ -42,8 +48,6 @@ export function Panel({
|
|
|
42
48
|
onToggle?.(!isExpanded);
|
|
43
49
|
};
|
|
44
50
|
|
|
45
|
-
useEffect(() => setExpanded(expanded), [expanded]);
|
|
46
|
-
|
|
47
51
|
return (
|
|
48
52
|
<PanelWrapper
|
|
49
53
|
className={className}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { SearchFacet, SearchFacetCount, SearchFilterItem } from '@redocly/theme/core/types';
|
|
@@ -27,15 +27,8 @@ export function SearchFilterFieldSelect({
|
|
|
27
27
|
const { useFacetQuery, useTranslate } = useThemeHooks();
|
|
28
28
|
const { translate } = useTranslate();
|
|
29
29
|
const { searchFacet, setSearchFacet, setSearchFacetQuery } = useFacetQuery(facet.field);
|
|
30
|
-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
31
|
-
const [selectOptions, setSelectOptions] = useState<any[]>([]);
|
|
32
30
|
const isMultiple = facet.type === 'multi-select';
|
|
33
31
|
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
setSelectOptions(getSelectOptions());
|
|
36
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
-
}, [searchFacet, facet]);
|
|
38
|
-
|
|
39
32
|
const onSearch = (value: string | null) => {
|
|
40
33
|
if (value === null) {
|
|
41
34
|
setSearchFacet(null);
|
|
@@ -49,38 +42,10 @@ export function SearchFilterFieldSelect({
|
|
|
49
42
|
}
|
|
50
43
|
};
|
|
51
44
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const facetValueToSelectOption = (facetValue?: SearchFacetCount | string) => {
|
|
59
|
-
if (!facetValue) {
|
|
60
|
-
return { element: null, value: '' };
|
|
61
|
-
}
|
|
62
|
-
if (typeof facetValue === 'string') {
|
|
63
|
-
return {
|
|
64
|
-
element: (
|
|
65
|
-
<FilterSelectOptionWrapper>
|
|
66
|
-
<FilterSelectOptionText>{facetValue}</FilterSelectOptionText>
|
|
67
|
-
</FilterSelectOptionWrapper>
|
|
68
|
-
),
|
|
69
|
-
value: facetValue,
|
|
70
|
-
};
|
|
71
|
-
} else {
|
|
72
|
-
const { value, count, isCounterVisible } = facetValue as SearchFacetCount;
|
|
73
|
-
return {
|
|
74
|
-
element: (
|
|
75
|
-
<FilterSelectOptionWrapper>
|
|
76
|
-
<FilterSelectOptionText>{value}</FilterSelectOptionText>
|
|
77
|
-
{isCounterVisible && <CounterTag>{count}</CounterTag>}
|
|
78
|
-
</FilterSelectOptionWrapper>
|
|
79
|
-
),
|
|
80
|
-
value: value,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
};
|
|
45
|
+
const selectOptions = useMemo(
|
|
46
|
+
() => (searchFacet ? searchFacet.values : facet.values).map(facetValueToSelectOption),
|
|
47
|
+
[searchFacet, facet],
|
|
48
|
+
);
|
|
84
49
|
|
|
85
50
|
return (
|
|
86
51
|
<Select
|
|
@@ -113,6 +78,33 @@ export function SearchFilterFieldSelect({
|
|
|
113
78
|
);
|
|
114
79
|
}
|
|
115
80
|
|
|
81
|
+
function facetValueToSelectOption(facetValue?: SearchFacetCount | string) {
|
|
82
|
+
if (!facetValue) {
|
|
83
|
+
return { element: null, value: '' };
|
|
84
|
+
}
|
|
85
|
+
if (typeof facetValue === 'string') {
|
|
86
|
+
return {
|
|
87
|
+
element: (
|
|
88
|
+
<FilterSelectOptionWrapper>
|
|
89
|
+
<FilterSelectOptionText>{facetValue}</FilterSelectOptionText>
|
|
90
|
+
</FilterSelectOptionWrapper>
|
|
91
|
+
),
|
|
92
|
+
value: facetValue,
|
|
93
|
+
};
|
|
94
|
+
} else {
|
|
95
|
+
const { value, count, isCounterVisible } = facetValue as SearchFacetCount;
|
|
96
|
+
return {
|
|
97
|
+
element: (
|
|
98
|
+
<FilterSelectOptionWrapper>
|
|
99
|
+
<FilterSelectOptionText>{value}</FilterSelectOptionText>
|
|
100
|
+
{isCounterVisible && <CounterTag>{count}</CounterTag>}
|
|
101
|
+
</FilterSelectOptionWrapper>
|
|
102
|
+
),
|
|
103
|
+
value: value,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
116
108
|
const FilterSelectOptionWrapper = styled.div`
|
|
117
109
|
width: 100%;
|
|
118
110
|
display: flex;
|
|
@@ -94,6 +94,7 @@ export function SearchAiDialog({
|
|
|
94
94
|
if (!initialMessage?.trim().length) {
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- appends the initial message into the conversation state lifted to the search root once the dialog mounts
|
|
97
98
|
setConversation((prev) => [
|
|
98
99
|
...prev,
|
|
99
100
|
{ role: AiSearchConversationRole.USER, content: initialMessage },
|
|
@@ -105,6 +106,7 @@ export function SearchAiDialog({
|
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- merges the streamed assistant response into the lifted conversation state as chunks arrive
|
|
108
110
|
setConversation((prev) => {
|
|
109
111
|
const lastMessage = prev[prev.length - 1];
|
|
110
112
|
const content: string = response || '';
|
|
@@ -146,6 +148,7 @@ export function SearchAiDialog({
|
|
|
146
148
|
|
|
147
149
|
useEffect(() => {
|
|
148
150
|
if (error) {
|
|
151
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- drops the pending message from the lifted conversation state once the request errors
|
|
149
152
|
setConversation((prev) => prev.slice(0, -1));
|
|
150
153
|
}
|
|
151
154
|
}, [error, setConversation]);
|
|
@@ -114,7 +114,12 @@ export function SearchItem({
|
|
|
114
114
|
<div>
|
|
115
115
|
{searchHighlight(parameter.place)}
|
|
116
116
|
{` → `}
|
|
117
|
-
{parameter.path?.length ?
|
|
117
|
+
{parameter.path?.length ? (
|
|
118
|
+
<>
|
|
119
|
+
{searchHighlight(parameter.path.join(' → '))}
|
|
120
|
+
{` → `}
|
|
121
|
+
</>
|
|
122
|
+
) : null}
|
|
118
123
|
{searchHighlight(parameter.name)}
|
|
119
124
|
</div>
|
|
120
125
|
<div>{searchHighlight(parameter.description)}</div>
|
|
@@ -97,6 +97,7 @@ export const search = css`
|
|
|
97
97
|
--search-message-line-height: var(--line-height-lg); // @presenter LineHeight
|
|
98
98
|
--search-message-text-color: var(--text-color-secondary); // @presenter Color
|
|
99
99
|
--search-message-gap: var(--spacing-md);
|
|
100
|
+
--search-message-padding: var(--spacing-md);
|
|
100
101
|
|
|
101
102
|
--search-header-buttons-gap: var(--spacing-sm);
|
|
102
103
|
--search-header-buttons-padding-left: var(--spacing-sm);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Fragment,
|
|
1
|
+
import React, { Fragment, useId, useRef, useState } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX } from 'react';
|
|
@@ -68,14 +68,20 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
|
|
|
68
68
|
setDropdownActive(false);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
const [prevOptions, setPrevOptions] = useState(options);
|
|
72
|
+
const [prevValue, setPrevValue] = useState(value);
|
|
73
|
+
const [prevMultiple, setPrevMultiple] = useState(multiple);
|
|
74
|
+
|
|
75
|
+
if (prevOptions !== options) {
|
|
76
|
+
setPrevOptions(options);
|
|
72
77
|
setFilteredOptions(options);
|
|
73
|
-
}
|
|
78
|
+
}
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
if (prevValue !== value || prevMultiple !== multiple) {
|
|
81
|
+
setPrevValue(value);
|
|
82
|
+
setPrevMultiple(multiple);
|
|
76
83
|
setSelectedOptions(getSelectedOptionsFromPropsValue());
|
|
77
|
-
|
|
78
|
-
}, [multiple, value]);
|
|
84
|
+
}
|
|
79
85
|
|
|
80
86
|
const changeSearchValue = (newSearchValue: string | null) => {
|
|
81
87
|
if (newSearchValue === searchValue) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX } from 'react';
|
|
@@ -36,11 +36,7 @@ export function Sidebar({
|
|
|
36
36
|
menuItemsAnimation,
|
|
37
37
|
}: SidebarProps): JSX.Element | null {
|
|
38
38
|
const { isOpen } = useMobileMenu();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
setMappedCollapsed(collapsed && !isOpen);
|
|
43
|
-
}, [isOpen, collapsed, setMappedCollapsed]);
|
|
39
|
+
const mappedCollapsed = collapsed && !isOpen;
|
|
44
40
|
|
|
45
41
|
const { sidebar } = useThemeConfig();
|
|
46
42
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether the first heading inside the enclosing `Markdown` mounts the page actions.
|
|
5
|
+
* Without a provider `Heading` falls back to the page type in the page metadata.
|
|
6
|
+
*/
|
|
7
|
+
export const PageActionsContext = createContext<boolean | null>(null);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './ThemeDataContext';
|
|
2
2
|
export * from './MarkdownLinkContext';
|
|
3
|
+
export * from './PageActionsContext';
|
|
3
4
|
export * from './CodeWalkthrough/CodeWalkthroughControlsContext';
|
|
4
5
|
export * from './CodeWalkthrough/CodeWalkthroughStepsContext';
|
|
6
|
+
export * from './AccordionGroupContext';
|
|
5
7
|
export * from './CodeSnippetContext';
|
|
6
8
|
export * from './SearchContext';
|
|
7
9
|
export * from './Toast/ToastContext';
|
|
@@ -18,6 +18,8 @@ export const useThemeHooks = vi.fn(() => ({
|
|
|
18
18
|
useTelemetry: vi.fn(() => ({
|
|
19
19
|
send: vi.fn(),
|
|
20
20
|
sendCodeSnippetReportedMessage: vi.fn(),
|
|
21
|
+
sendConnectMcpClickedMessage: vi.fn(),
|
|
22
|
+
sendMcpConnectionSnippetCopiedMessage: vi.fn(),
|
|
21
23
|
sendPageActionsButtonClickedMessage: vi.fn(),
|
|
22
24
|
sendColorModeSwitchedMessage: vi.fn(),
|
|
23
25
|
})),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router';
|
|
3
3
|
|
|
4
4
|
import type {
|
|
@@ -50,10 +50,6 @@ export function useCodeWalkthroughControls(
|
|
|
50
50
|
const navigate = useNavigate();
|
|
51
51
|
const searchParams = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
|
52
52
|
|
|
53
|
-
const filtersRef = useRef(filters);
|
|
54
|
-
const inputsRef = useRef(inputs);
|
|
55
|
-
const togglesRef = useRef(toggles);
|
|
56
|
-
|
|
57
53
|
const getInitialState = () => {
|
|
58
54
|
const state: CodeWalkthroughControlsState = {};
|
|
59
55
|
|
|
@@ -90,20 +86,11 @@ export function useCodeWalkthroughControls(
|
|
|
90
86
|
|
|
91
87
|
const [controlsState, setControlsState] = useState(getInitialState);
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
JSON.stringify(filters) === JSON.stringify(filtersRef.current),
|
|
96
|
-
JSON.stringify(inputs) === JSON.stringify(inputsRef.current),
|
|
97
|
-
JSON.stringify(toggles) === JSON.stringify(togglesRef.current),
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
if (sameProps.every(Boolean)) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
89
|
+
const controlsKey = JSON.stringify([filters, inputs, toggles]);
|
|
90
|
+
const [prevControlsKey, setPrevControlsKey] = useState(controlsKey);
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
togglesRef.current = toggles;
|
|
92
|
+
if (prevControlsKey !== controlsKey) {
|
|
93
|
+
setPrevControlsKey(controlsKey);
|
|
107
94
|
|
|
108
95
|
const newState = getInitialState();
|
|
109
96
|
|
|
@@ -120,8 +107,7 @@ export function useCodeWalkthroughControls(
|
|
|
120
107
|
});
|
|
121
108
|
|
|
122
109
|
setControlsState(newState);
|
|
123
|
-
|
|
124
|
-
}, [filters, inputs, toggles, enableDeepLink]);
|
|
110
|
+
}
|
|
125
111
|
|
|
126
112
|
const changeControlState = (id: string, value: string | boolean) => {
|
|
127
113
|
setControlsState((prev) => {
|
|
@@ -95,6 +95,7 @@ export function useCodeWalkthroughSteps({
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- marker geometry is derived from post-layout measurements (offsetTop, clientHeight, window.innerHeight)
|
|
98
99
|
setMarkers(
|
|
99
100
|
markers.reduce(
|
|
100
101
|
(acc, marker, index) => {
|
|
@@ -252,6 +253,7 @@ export function useCodeWalkthroughSteps({
|
|
|
252
253
|
|
|
253
254
|
const rootTopOffset = root.current?.offsetTop ?? 0;
|
|
254
255
|
if (!activeStep && rootTopOffset <= options.navbarHeight) {
|
|
256
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- seeded from root.current.offsetTop, a post-layout measurement
|
|
255
257
|
setActiveStep(visibleSteps[0]?.id || null);
|
|
256
258
|
}
|
|
257
259
|
|
|
@@ -16,6 +16,7 @@ export function useMenuItemExpanded(item: ItemState): [boolean, Dispatch<SetStat
|
|
|
16
16
|
const [controlledValue, setControlledValue] = useState(isItemExpanded(item));
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- deferred a commit on purpose so CSS transitions still run
|
|
19
20
|
setIsExpanded(controlledValue);
|
|
20
21
|
}, [controlledValue]);
|
|
21
22
|
|
|
@@ -286,18 +286,22 @@ export function useActiveHeading(
|
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
const headers = findHeaders(contentElement);
|
|
289
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- headings and the initial active heading come from DOM queries and scroll measurement, available only after layout
|
|
289
290
|
setHeadingElements(headers);
|
|
290
291
|
|
|
291
292
|
if (headers.length > 0 && !heading) {
|
|
292
293
|
if (isTopOfPage()) {
|
|
294
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
293
295
|
setHeading(headers[0].id);
|
|
294
296
|
} else {
|
|
295
297
|
const totalHeight = scrollY + window.innerHeight;
|
|
296
298
|
if (totalHeight >= document.body.scrollHeight - 10) {
|
|
299
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
297
300
|
setHeading(headers[headers.length - 1]?.id || undefined);
|
|
298
301
|
} else {
|
|
299
302
|
const triggerOffset = getTriggerOffset();
|
|
300
303
|
const activeHeader = findActiveHeaderByVirtualPosition(headers, scrollY, triggerOffset);
|
|
304
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
301
305
|
setHeading(activeHeader.id);
|
|
302
306
|
}
|
|
303
307
|
}
|
|
@@ -8,6 +8,7 @@ import { useMCPConfig } from './use-mcp-config';
|
|
|
8
8
|
import { ClipboardService } from '../utils/clipboard-service';
|
|
9
9
|
import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
|
|
10
10
|
import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
|
|
11
|
+
import { getBaseDataAttributes } from '../utils/telemetry/get-base-data-attributes';
|
|
11
12
|
|
|
12
13
|
export type McpButtonOptions = {
|
|
13
14
|
options?: MCPOption[];
|
|
@@ -27,8 +28,9 @@ const COPIED_RESET_TIMEOUT = 1000;
|
|
|
27
28
|
export function useConnectMCPButton({
|
|
28
29
|
options = DEFAULT_MCP_OPTIONS,
|
|
29
30
|
}: McpButtonOptions = {}): McpButtonSettings {
|
|
30
|
-
const { useTranslate } = useThemeHooks();
|
|
31
|
+
const { useTranslate, useTelemetry } = useThemeHooks();
|
|
31
32
|
const { translate } = useTranslate();
|
|
33
|
+
const telemetry = useTelemetry();
|
|
32
34
|
const { serverName, serverUrl, cursorUrl, vscodeUrl } = useMCPConfig();
|
|
33
35
|
const [copiedOption, setCopiedOption] = useState<MCPOption | null>(null);
|
|
34
36
|
|
|
@@ -40,6 +42,10 @@ export function useConnectMCPButton({
|
|
|
40
42
|
|
|
41
43
|
const handleAction = useCallback(
|
|
42
44
|
(action: MCPOption) => {
|
|
45
|
+
telemetry.sendConnectMcpClickedMessage([
|
|
46
|
+
{ ...getBaseDataAttributes('connectMcp', 'button'), option: action },
|
|
47
|
+
]);
|
|
48
|
+
|
|
43
49
|
const connection = { serverName, url: serverUrl };
|
|
44
50
|
const copyTextMap: Partial<Record<MCPOption, string>> = {
|
|
45
51
|
copy: JSON.stringify(
|
|
@@ -66,7 +72,7 @@ export function useConnectMCPButton({
|
|
|
66
72
|
|
|
67
73
|
window.open(urlMap[action], '_blank');
|
|
68
74
|
},
|
|
69
|
-
[cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied],
|
|
75
|
+
[cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied, telemetry],
|
|
70
76
|
);
|
|
71
77
|
|
|
72
78
|
const triggerButtonText = useMemo(
|
|
@@ -21,6 +21,7 @@ export function useLocalState<T>(key: string, initialValue: T): [T, (value: T) =
|
|
|
21
21
|
// then client loads the actual stored value without hydration mismatch
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
if (!isBrowser()) return;
|
|
24
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-derived-state -- localStorage is read after mount so SSR and client hydrate from the same value
|
|
24
25
|
setValue(getStoredValue(key, initialValue));
|
|
25
26
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
27
|
}, [key]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react';
|
|
2
2
|
import { useLocation } from 'react-router';
|
|
3
3
|
|
|
4
4
|
import { useModalScrollLock } from './use-modal-scroll-lock';
|
|
@@ -6,11 +6,19 @@ import { useModalScrollLock } from './use-modal-scroll-lock';
|
|
|
6
6
|
export function useMobileMenu(initialState = false) {
|
|
7
7
|
const location = useLocation();
|
|
8
8
|
const [isOpen, setIsOpen] = useState(initialState);
|
|
9
|
+
const locationKey = `${location.pathname}${location.hash}`;
|
|
10
|
+
const [prevLocationKey, setPrevLocationKey] = useState(locationKey);
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
if (isOpen && prevLocationKey !== locationKey) {
|
|
13
|
+
setPrevLocationKey(locationKey);
|
|
14
|
+
setIsOpen(false);
|
|
15
|
+
}
|
|
11
16
|
useModalScrollLock(isOpen);
|
|
12
17
|
|
|
13
|
-
const openMobileMenu = () =>
|
|
18
|
+
const openMobileMenu = () => {
|
|
19
|
+
setPrevLocationKey(locationKey);
|
|
20
|
+
setIsOpen(true);
|
|
21
|
+
};
|
|
14
22
|
const closeMobileMenu = () => setIsOpen(false);
|
|
15
23
|
|
|
16
24
|
return { isOpen, openMobileMenu, closeMobileMenu };
|
|
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router';
|
|
|
4
4
|
import { getProductClassName } from '@redocly/theme/components/Product/utils';
|
|
5
5
|
|
|
6
6
|
import { useThemeHooks } from './use-theme-hooks';
|
|
7
|
-
import { withPathPrefix, generateBeforeAfterContext } from '../utils';
|
|
7
|
+
import { withPathPrefix, generateBeforeAfterContext, getSafeClassName } from '../utils';
|
|
8
8
|
|
|
9
9
|
export function useProductPicker() {
|
|
10
10
|
const { useCurrentProduct, useProducts, useTelemetry, useLoadAndNavigate } = useThemeHooks();
|
|
@@ -36,7 +36,7 @@ export function useProductPicker() {
|
|
|
36
36
|
Array.from(root.classList)
|
|
37
37
|
.filter((c) => c.startsWith('product-'))
|
|
38
38
|
.forEach((c) => root.classList.remove(c));
|
|
39
|
-
root.classList.add(getProductClassName(freshProduct.name));
|
|
39
|
+
root.classList.add(getSafeClassName(getProductClassName(freshProduct.name)));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
loadAndNavigate({ navigate, to: withPathPrefix(freshProduct.link) });
|
|
@@ -289,14 +289,16 @@ export const useActiveTab = ({ initialTab, tabsId }: UseActiveTabProps) => {
|
|
|
289
289
|
const [activeTab, setActiveTab] = useState<string | undefined>(undefined);
|
|
290
290
|
const resolvedActiveTab = activeTab ?? initialTab;
|
|
291
291
|
const prevActiveTabRef = useRef(resolvedActiveTab);
|
|
292
|
+
const isTabResolvedFromHashRef = useRef(false);
|
|
292
293
|
|
|
293
294
|
useEffect(() => {
|
|
294
|
-
if (
|
|
295
|
+
if (isTabResolvedFromHashRef.current) {
|
|
295
296
|
return;
|
|
296
297
|
}
|
|
298
|
+
isTabResolvedFromHashRef.current = true;
|
|
297
299
|
|
|
298
300
|
setActiveTab(getInitialTab({ initialTab, hash: location.hash, tabsId }));
|
|
299
|
-
}, [
|
|
301
|
+
}, [initialTab, location.hash, tabsId]);
|
|
300
302
|
|
|
301
303
|
useEffect(() => {
|
|
302
304
|
const hasActiveTabChanged = prevActiveTabRef.current !== resolvedActiveTab;
|
|
@@ -36,6 +36,8 @@ export const useTelemetryFallback = () => ({
|
|
|
36
36
|
sendProductPickerChangedMessage: () => {},
|
|
37
37
|
sendLogoClickedMessage: () => {},
|
|
38
38
|
sendCodeSnippetReportedMessage: () => {},
|
|
39
|
+
sendConnectMcpClickedMessage: () => {},
|
|
40
|
+
sendMcpConnectionSnippetCopiedMessage: () => {},
|
|
39
41
|
sendMobileMenuClosedMessage: () => {},
|
|
40
42
|
sendMobileMenuOpenedMessage: () => {},
|
|
41
43
|
sendPageViewedMessage: () => {},
|