@redocly/theme 0.69.0-next.9 → 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/Catalog/CatalogEntities.js +1 -0
- 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/Search/SearchAiDialog.js +3 -0
- 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-steps.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-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/Catalog/CatalogEntities.tsx +1 -0
- 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/Search/SearchAiDialog.tsx +3 -0
- 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-steps.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-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
|
@@ -70,6 +70,7 @@ function CatalogEntities(props) {
|
|
|
70
70
|
const shouldShowLoadMore = query.hasNextPage ||
|
|
71
71
|
(query.isPlaceholderData && entities && entities.length >= LOAD_MORE_THRESHOLD);
|
|
72
72
|
(0, react_1.useEffect)(() => {
|
|
73
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- the entities query lives in this ejectable component; the fetched total is mirrored up so the portal page header can show the count
|
|
73
74
|
setEntitiesCounter(total || 0);
|
|
74
75
|
}, [total, setEntitiesCounter]);
|
|
75
76
|
if (!query.isPending && (!entities || entities.length === 0)) {
|
|
@@ -62,11 +62,10 @@ const TAB_LABELS = {
|
|
|
62
62
|
},
|
|
63
63
|
json: { translationKey: 'page.mcpNotAllowed.tabs.json', defaultLabel: 'JSON' },
|
|
64
64
|
};
|
|
65
|
-
// The report control is docs feedback, not useful on the connection page.
|
|
66
|
-
const CODE_BLOCK_HEADER = { controls: { copy: {}, report: { hidden: true } } };
|
|
67
65
|
function McpConnectionTabs({ options = DEFAULT_TAB_OPTIONS, className, }) {
|
|
68
|
-
const { useTranslate } = (0, hooks_1.useThemeHooks)();
|
|
66
|
+
const { useTranslate, useTelemetry } = (0, hooks_1.useThemeHooks)();
|
|
69
67
|
const { translate } = useTranslate();
|
|
68
|
+
const telemetry = useTelemetry();
|
|
70
69
|
const { serverName, serverUrl, isMcpDisabled } = (0, hooks_1.useMCPConfig)();
|
|
71
70
|
// The SSR markup of error pages is cached with the build-time origin, so the
|
|
72
71
|
// tabs remount after hydration to pick up the real browser origin.
|
|
@@ -76,20 +75,31 @@ function McpConnectionTabs({ options = DEFAULT_TAB_OPTIONS, className, }) {
|
|
|
76
75
|
return null;
|
|
77
76
|
}
|
|
78
77
|
const connection = { serverName, url: serverUrl };
|
|
78
|
+
const snippetHeader = (client, snippet) => ({
|
|
79
|
+
controls: {
|
|
80
|
+
copy: {
|
|
81
|
+
onClick: () => telemetry.sendMcpConnectionSnippetCopiedMessage([
|
|
82
|
+
Object.assign(Object.assign({}, (0, utils_1.getBaseDataAttributes)('mcpConnectionSnippet', 'button')), { client, snippet }),
|
|
83
|
+
]),
|
|
84
|
+
},
|
|
85
|
+
// The report control is docs feedback, not useful on the connection page.
|
|
86
|
+
report: { hidden: true },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
79
89
|
return (react_1.default.createElement(McpConnectionTabsWrapper, { key: hydrated ? 'client' : 'ssr', "data-component-name": "MCP/McpConnectionTabs", "data-testid": "mcp-connection-tabs", className: className },
|
|
80
90
|
react_1.default.createElement(Tabs_1.Tabs, { size: Tabs_1.TabsSize.SMALL }, options.map((option) => {
|
|
81
91
|
const { translationKey, defaultLabel, icon: TabIcon } = TAB_LABELS[option];
|
|
82
|
-
return (react_1.default.createElement(TabItemFragment_1.TabItemFragment, { key: option, label: translate(translationKey, defaultLabel), icon: TabIcon ? react_1.default.createElement(TabIcon, null) : undefined }, renderTabContent(option, connection, translate)));
|
|
92
|
+
return (react_1.default.createElement(TabItemFragment_1.TabItemFragment, { key: option, label: translate(translationKey, defaultLabel), icon: TabIcon ? react_1.default.createElement(TabIcon, null) : undefined }, renderTabContent(option, connection, translate, snippetHeader)));
|
|
83
93
|
}))));
|
|
84
94
|
}
|
|
85
|
-
function renderTabContent(option, connection, translate) {
|
|
95
|
+
function renderTabContent(option, connection, translate, snippetHeader) {
|
|
86
96
|
const terminalHint = (react_1.default.createElement(TabHint, null, translate('page.mcpNotAllowed.snippets.terminal.hint', 'Run this command in your terminal:')));
|
|
87
|
-
const urlBlock = react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "text", source: connection.url, header:
|
|
97
|
+
const urlBlock = (react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "text", source: connection.url, header: snippetHeader(option, 'chatgpt-app') }));
|
|
88
98
|
const assistantPromptClient = {
|
|
89
99
|
headline: translate('page.mcpNotAllowed.snippets.prompt.headline', 'Prompt'),
|
|
90
100
|
content: (react_1.default.createElement(react_1.default.Fragment, null,
|
|
91
101
|
react_1.default.createElement(TabHint, null, translate('page.mcpNotAllowed.snippets.prompt.hint', 'Or paste this prompt into the chat to have it add the server for you:')),
|
|
92
|
-
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "text", source: (0, utils_1.generateMcpInstallPrompt)(connection), header:
|
|
102
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "text", source: (0, utils_1.generateMcpInstallPrompt)(connection), header: snippetHeader(option, 'prompt'), wrapContents: true }))),
|
|
93
103
|
};
|
|
94
104
|
switch (option) {
|
|
95
105
|
case 'claude':
|
|
@@ -98,13 +108,13 @@ function renderTabContent(option, connection, translate) {
|
|
|
98
108
|
headline: translate('page.mcpNotAllowed.snippets.claudeCode.headline', 'Claude Code'),
|
|
99
109
|
content: (react_1.default.createElement(react_1.default.Fragment, null,
|
|
100
110
|
terminalHint,
|
|
101
|
-
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "shell", source: (0, utils_1.generateClaudeCodeCommand)(connection), header:
|
|
111
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "shell", source: (0, utils_1.generateClaudeCodeCommand)(connection), header: snippetHeader(option, 'claude-code'), wrapContents: true }))),
|
|
102
112
|
},
|
|
103
113
|
{
|
|
104
114
|
headline: translate('page.mcpNotAllowed.snippets.claudeDesktop.headline', 'Claude Desktop'),
|
|
105
115
|
content: (react_1.default.createElement(react_1.default.Fragment, null,
|
|
106
116
|
react_1.default.createElement(TabHint, null, translate('page.mcpNotAllowed.snippets.claudeDesktop.instruction', 'In Claude Desktop, open Settings → Developer → Edit Config, add this to the configuration file, and restart Claude Desktop:')),
|
|
107
|
-
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "json", source: (0, utils_1.generateClaudeDesktopConfig)(connection), header:
|
|
117
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "json", source: (0, utils_1.generateClaudeDesktopConfig)(connection), header: snippetHeader(option, 'claude-desktop') }))),
|
|
108
118
|
},
|
|
109
119
|
]);
|
|
110
120
|
case 'chatgpt':
|
|
@@ -113,7 +123,7 @@ function renderTabContent(option, connection, translate) {
|
|
|
113
123
|
headline: translate('page.mcpNotAllowed.snippets.codexCli.headline', 'Codex CLI'),
|
|
114
124
|
content: (react_1.default.createElement(react_1.default.Fragment, null,
|
|
115
125
|
terminalHint,
|
|
116
|
-
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "shell", source: (0, utils_1.generateCodexCommand)(connection), header:
|
|
126
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "shell", source: (0, utils_1.generateCodexCommand)(connection), header: snippetHeader(option, 'codex'), wrapContents: true }))),
|
|
117
127
|
},
|
|
118
128
|
{
|
|
119
129
|
headline: translate('page.mcpNotAllowed.snippets.chatgptApp.headline', 'ChatGPT desktop app'),
|
|
@@ -126,7 +136,7 @@ function renderTabContent(option, connection, translate) {
|
|
|
126
136
|
case 'json':
|
|
127
137
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
128
138
|
react_1.default.createElement(TabHint, null, translate('page.mcpNotAllowed.snippets.json.hint', 'Add this server entry to your MCP client configuration file:')),
|
|
129
|
-
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "json", source: JSON.stringify({ [connection.serverName]: { url: connection.url, description: 'MCP Server' } }, null, 2), header:
|
|
139
|
+
react_1.default.createElement(CodeBlock_1.CodeBlock, { lang: "json", source: JSON.stringify({ [connection.serverName]: { url: connection.url, description: 'MCP Server' } }, null, 2), header: snippetHeader(option, 'json') })));
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
function renderClientList(clients) {
|
|
@@ -178,10 +178,8 @@ exports.Markdown = styled_components_1.default.main
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
hr {
|
|
181
|
-
border-top: 1px solid var(--md-hr-border-color);
|
|
182
|
-
background-color: var(--md-hr-bg-color);
|
|
183
|
-
height: var(--md-hr-height);
|
|
184
181
|
margin: var(--md-hr-margin-vertical) 0;
|
|
182
|
+
border: var(--md-hr-border);
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
details:not(${constants_1.ACCORDION_SELECTOR}) {
|
|
@@ -46,6 +46,14 @@ exports.markdownBaseTableCss = (0, styled_components_1.css) `
|
|
|
46
46
|
background-color: var(--md-table-header-bg-color);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/* hide the header row when every cell is empty; one labelled cell keeps it visible */
|
|
50
|
+
thead:has(th:empty) {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
thead:has(th:not(:empty)) {
|
|
54
|
+
display: table-header-group;
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
tr {
|
|
50
58
|
background-color: var(--md-table-stripe-bg-color);
|
|
51
59
|
}
|
|
@@ -62,9 +62,8 @@ exports.markdown = (0, styled_components_1.css) `
|
|
|
62
62
|
* @tokens Markdown Horizontal Rule
|
|
63
63
|
*/
|
|
64
64
|
|
|
65
|
-
--md-hr-bg-color: transparent; // @presenter Color
|
|
66
65
|
--md-hr-border-color: var(--border-color-secondary); // @presenter Color
|
|
67
|
-
--md-hr-
|
|
66
|
+
--md-hr-border: 0.5px solid var(--md-hr-border-color); // @presenter Border
|
|
68
67
|
--md-hr-margin-vertical: 20px; // @presenter Spacing
|
|
69
68
|
|
|
70
69
|
/**
|
|
@@ -80,6 +80,7 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
|
|
|
80
80
|
if (!(initialMessage === null || initialMessage === void 0 ? void 0 : initialMessage.trim().length)) {
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
+
// 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
|
|
83
84
|
setConversation((prev) => [
|
|
84
85
|
...prev,
|
|
85
86
|
{ role: constants_1.AiSearchConversationRole.USER, content: initialMessage },
|
|
@@ -89,6 +90,7 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
|
|
|
89
90
|
if (response === undefined || conversation.length === 0 || error) {
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
93
|
+
// 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
|
|
92
94
|
setConversation((prev) => {
|
|
93
95
|
const lastMessage = prev[prev.length - 1];
|
|
94
96
|
const content = response || '';
|
|
@@ -127,6 +129,7 @@ function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error,
|
|
|
127
129
|
]);
|
|
128
130
|
(0, react_1.useEffect)(() => {
|
|
129
131
|
if (error) {
|
|
132
|
+
// 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
|
|
130
133
|
setConversation((prev) => prev.slice(0, -1));
|
|
131
134
|
}
|
|
132
135
|
}, [error, setConversation]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AccordionGroupContext: import("react").Context<string | null>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AccordionGroupContext = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
exports.AccordionGroupContext = (0, react_1.createContext)(null);
|
|
6
|
+
//# sourceMappingURL=AccordionGroupContext.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PageActionsContext = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Whether the first heading inside the enclosing `Markdown` mounts the page actions.
|
|
7
|
+
* Without a provider `Heading` falls back to the page type in the page metadata.
|
|
8
|
+
*/
|
|
9
|
+
exports.PageActionsContext = (0, react_1.createContext)(null);
|
|
10
|
+
//# sourceMappingURL=PageActionsContext.js.map
|
|
@@ -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';
|
|
@@ -16,8 +16,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./ThemeDataContext"), exports);
|
|
18
18
|
__exportStar(require("./MarkdownLinkContext"), exports);
|
|
19
|
+
__exportStar(require("./PageActionsContext"), exports);
|
|
19
20
|
__exportStar(require("./CodeWalkthrough/CodeWalkthroughControlsContext"), exports);
|
|
20
21
|
__exportStar(require("./CodeWalkthrough/CodeWalkthroughStepsContext"), exports);
|
|
22
|
+
__exportStar(require("./AccordionGroupContext"), exports);
|
|
21
23
|
__exportStar(require("./CodeSnippetContext"), exports);
|
|
22
24
|
__exportStar(require("./SearchContext"), exports);
|
|
23
25
|
__exportStar(require("./Toast/ToastContext"), exports);
|
|
@@ -58,6 +58,7 @@ function useCodeWalkthroughSteps({ steps, enableDeepLink, root, }) {
|
|
|
58
58
|
startOffset: markersMinTopOffset < endOffset ? startOffset : 0,
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
// 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)
|
|
61
62
|
setMarkers(markers.reduce((acc, marker, index) => {
|
|
62
63
|
var _a;
|
|
63
64
|
const step = visibleSteps[index];
|
|
@@ -205,19 +205,23 @@ function useActiveHeading(contentElement, displayedHeadings) {
|
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
207
|
const headers = findHeaders(contentElement);
|
|
208
|
+
// 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
|
|
208
209
|
setHeadingElements(headers);
|
|
209
210
|
if (headers.length > 0 && !heading) {
|
|
210
211
|
if (isTopOfPage()) {
|
|
212
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
211
213
|
setHeading(headers[0].id);
|
|
212
214
|
}
|
|
213
215
|
else {
|
|
214
216
|
const totalHeight = scrollY + window.innerHeight;
|
|
215
217
|
if (totalHeight >= document.body.scrollHeight - 10) {
|
|
218
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
216
219
|
setHeading(((_a = headers[headers.length - 1]) === null || _a === void 0 ? void 0 : _a.id) || undefined);
|
|
217
220
|
}
|
|
218
221
|
else {
|
|
219
222
|
const triggerOffset = getTriggerOffset();
|
|
220
223
|
const activeHeader = findActiveHeaderByVirtualPosition(headers, scrollY, triggerOffset);
|
|
224
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates -- see above
|
|
221
225
|
setHeading(activeHeader.id);
|
|
222
226
|
}
|
|
223
227
|
}
|
|
@@ -11,15 +11,20 @@ const use_mcp_config_1 = require("./use-mcp-config");
|
|
|
11
11
|
const clipboard_service_1 = require("../utils/clipboard-service");
|
|
12
12
|
const mcp_1 = require("../constants/mcp");
|
|
13
13
|
const mcp_2 = require("../utils/mcp");
|
|
14
|
+
const get_base_data_attributes_1 = require("../utils/telemetry/get-base-data-attributes");
|
|
14
15
|
const COPIED_RESET_TIMEOUT = 1000;
|
|
15
16
|
function useConnectMCPButton({ options = mcp_1.DEFAULT_MCP_OPTIONS, } = {}) {
|
|
16
|
-
const { useTranslate } = (0, use_theme_hooks_1.useThemeHooks)();
|
|
17
|
+
const { useTranslate, useTelemetry } = (0, use_theme_hooks_1.useThemeHooks)();
|
|
17
18
|
const { translate } = useTranslate();
|
|
19
|
+
const telemetry = useTelemetry();
|
|
18
20
|
const { serverName, serverUrl, cursorUrl, vscodeUrl } = (0, use_mcp_config_1.useMCPConfig)();
|
|
19
21
|
const [copiedOption, setCopiedOption] = (0, react_1.useState)(null);
|
|
20
22
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
23
|
const resetCopied = (0, react_1.useCallback)((0, lodash_debounce_1.default)(() => setCopiedOption(null), COPIED_RESET_TIMEOUT), []);
|
|
22
24
|
const handleAction = (0, react_1.useCallback)((action) => {
|
|
25
|
+
telemetry.sendConnectMcpClickedMessage([
|
|
26
|
+
Object.assign(Object.assign({}, (0, get_base_data_attributes_1.getBaseDataAttributes)('connectMcp', 'button')), { option: action }),
|
|
27
|
+
]);
|
|
23
28
|
const connection = { serverName, url: serverUrl };
|
|
24
29
|
const copyTextMap = {
|
|
25
30
|
copy: JSON.stringify({ [serverName]: { url: serverUrl, description: 'MCP Server' } }, null, 2),
|
|
@@ -38,7 +43,7 @@ function useConnectMCPButton({ options = mcp_1.DEFAULT_MCP_OPTIONS, } = {}) {
|
|
|
38
43
|
vscode: vscodeUrl,
|
|
39
44
|
};
|
|
40
45
|
window.open(urlMap[action], '_blank');
|
|
41
|
-
}, [cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied]);
|
|
46
|
+
}, [cursorUrl, vscodeUrl, serverUrl, serverName, resetCopied, telemetry]);
|
|
42
47
|
const triggerButtonText = (0, react_1.useMemo)(() => translate('page.actions.connectMcp', 'Connect MCP'), [translate]);
|
|
43
48
|
const visibleOptions = (0, react_1.useMemo)(() => options.filter(Boolean), [options]);
|
|
44
49
|
return {
|
|
@@ -33,7 +33,7 @@ function useProductPicker() {
|
|
|
33
33
|
Array.from(root.classList)
|
|
34
34
|
.filter((c) => c.startsWith('product-'))
|
|
35
35
|
.forEach((c) => root.classList.remove(c));
|
|
36
|
-
root.classList.add((0, utils_1.getProductClassName)(freshProduct.name));
|
|
36
|
+
root.classList.add((0, utils_2.getSafeClassName)((0, utils_1.getProductClassName)(freshProduct.name)));
|
|
37
37
|
}
|
|
38
38
|
loadAndNavigate({ navigate, to: (0, utils_2.withPathPrefix)(freshProduct.link) });
|
|
39
39
|
}
|
|
@@ -220,12 +220,14 @@ const useActiveTab = ({ initialTab, tabsId }) => {
|
|
|
220
220
|
const [activeTab, setActiveTab] = (0, react_1.useState)(undefined);
|
|
221
221
|
const resolvedActiveTab = activeTab !== null && activeTab !== void 0 ? activeTab : initialTab;
|
|
222
222
|
const prevActiveTabRef = (0, react_1.useRef)(resolvedActiveTab);
|
|
223
|
+
const isTabResolvedFromHashRef = (0, react_1.useRef)(false);
|
|
223
224
|
(0, react_1.useEffect)(() => {
|
|
224
|
-
if (
|
|
225
|
+
if (isTabResolvedFromHashRef.current) {
|
|
225
226
|
return;
|
|
226
227
|
}
|
|
228
|
+
isTabResolvedFromHashRef.current = true;
|
|
227
229
|
setActiveTab(getInitialTab({ initialTab, hash: location.hash, tabsId }));
|
|
228
|
-
}, [
|
|
230
|
+
}, [initialTab, location.hash, tabsId]);
|
|
229
231
|
(0, react_1.useEffect)(() => {
|
|
230
232
|
const hasActiveTabChanged = prevActiveTabRef.current !== resolvedActiveTab;
|
|
231
233
|
if (!tabsId || !hasActiveTabChanged) {
|
|
@@ -34,6 +34,8 @@ export declare const useTelemetryFallback: () => {
|
|
|
34
34
|
sendProductPickerChangedMessage: () => void;
|
|
35
35
|
sendLogoClickedMessage: () => void;
|
|
36
36
|
sendCodeSnippetReportedMessage: () => void;
|
|
37
|
+
sendConnectMcpClickedMessage: () => void;
|
|
38
|
+
sendMcpConnectionSnippetCopiedMessage: () => void;
|
|
37
39
|
sendMobileMenuClosedMessage: () => void;
|
|
38
40
|
sendMobileMenuOpenedMessage: () => void;
|
|
39
41
|
sendPageViewedMessage: () => void;
|
|
@@ -39,6 +39,8 @@ const useTelemetryFallback = () => ({
|
|
|
39
39
|
sendProductPickerChangedMessage: () => { },
|
|
40
40
|
sendLogoClickedMessage: () => { },
|
|
41
41
|
sendCodeSnippetReportedMessage: () => { },
|
|
42
|
+
sendConnectMcpClickedMessage: () => { },
|
|
43
|
+
sendMcpConnectionSnippetCopiedMessage: () => { },
|
|
42
44
|
sendMobileMenuClosedMessage: () => { },
|
|
43
45
|
sendMobileMenuOpenedMessage: () => { },
|
|
44
46
|
sendPageViewedMessage: () => { },
|
|
@@ -592,6 +592,7 @@ const links = (0, styled_components_1.css) `
|
|
|
592
592
|
|
|
593
593
|
// @tokens End
|
|
594
594
|
`;
|
|
595
|
+
// oxlint-disable-next-line theme/css-variable-pattern -- `--schema-labels--text-color` is a documented public variable
|
|
595
596
|
const apiReferenceDocs = (0, styled_components_1.css) `
|
|
596
597
|
/* === ref docs and graphql docs specific === */
|
|
597
598
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSafeClassName = getSafeClassName;
|
|
3
4
|
exports.concatClassNames = concatClassNames;
|
|
4
5
|
const SPACE_DELIMITER = ' ';
|
|
6
|
+
// EasyList/AdGuard ship generic cosmetic rules that hide these exact class tokens on every
|
|
7
|
+
// site; the trailing underscore keeps the class usable without matching the filter lists.
|
|
8
|
+
const ADBLOCK_HIDDEN_CLASS_NAMES = new Set(['product-ad', 'product-ads', 'product-inlist-ad']);
|
|
9
|
+
function getSafeClassName(className) {
|
|
10
|
+
return ADBLOCK_HIDDEN_CLASS_NAMES.has(className) ? `${className}_` : className;
|
|
11
|
+
}
|
|
5
12
|
function concatClassNames(...args) {
|
|
6
13
|
return args
|
|
7
14
|
.map((className) => (className ? className.trim() : className))
|
|
@@ -32,21 +32,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.Accordion = Accordion;
|
|
40
|
-
const react_1 =
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
41
38
|
const styled_components_1 = __importStar(require("styled-components"));
|
|
42
39
|
const ChevronRightIcon_1 = require("../../../icons/ChevronRightIcon/ChevronRightIcon");
|
|
43
40
|
const constants_1 = require("../../../core/constants");
|
|
41
|
+
const contexts_1 = require("../../../core/contexts");
|
|
44
42
|
function Accordion({ title, expanded = false, className, children, 'data-source': dataSource, 'data-hash': dataHash, }) {
|
|
45
|
-
|
|
43
|
+
const groupName = (0, react_1.useContext)(contexts_1.AccordionGroupContext);
|
|
44
|
+
return (react_1.default.createElement(AccordionWrapper, { className: className, open: expanded, name: groupName !== null && groupName !== void 0 ? groupName : undefined, "data-component-name": constants_1.ACCORDION_COMPONENT_NAME, "data-source": dataSource, "data-hash": dataHash },
|
|
46
45
|
react_1.default.createElement(AccordionSummary, null,
|
|
47
46
|
react_1.default.createElement(ChevronRightIcon_1.ChevronRightIcon, { color: "--md-accordion-chevron-icon-color", size: "var(--md-accordion-chevron-icon-size)" }),
|
|
48
47
|
title),
|
|
49
|
-
react_1.default.createElement(AccordionBody, null,
|
|
48
|
+
react_1.default.createElement(AccordionBody, null,
|
|
49
|
+
react_1.default.createElement(contexts_1.AccordionGroupContext.Provider, { value: null }, children))));
|
|
50
50
|
}
|
|
51
51
|
const AccordionWrapper = styled_components_1.default.details `
|
|
52
52
|
margin: var(--md-accordion-margin);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX, PropsWithChildren } from 'react';
|
|
2
2
|
export type AccordionGroupProps = {
|
|
3
|
+
singleExpanded?: boolean;
|
|
3
4
|
className?: string;
|
|
4
5
|
};
|
|
5
|
-
export declare function AccordionGroup({ className, children, }: PropsWithChildren<AccordionGroupProps>): JSX.Element;
|
|
6
|
+
export declare function AccordionGroup({ singleExpanded, className, children, }: PropsWithChildren<AccordionGroupProps>): JSX.Element;
|
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.AccordionGroup = AccordionGroup;
|
|
7
|
-
const react_1 =
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
8
41
|
const styled_components_1 = __importDefault(require("styled-components"));
|
|
9
42
|
const constants_1 = require("../../../core/constants");
|
|
10
|
-
|
|
11
|
-
|
|
43
|
+
const contexts_1 = require("../../../core/contexts");
|
|
44
|
+
function AccordionGroup({ singleExpanded = false, className, children, }) {
|
|
45
|
+
const groupName = (0, react_1.useId)();
|
|
46
|
+
return (react_1.default.createElement(AccordionGroupWrapper, { className: className, "data-component-name": constants_1.ACCORDION_GROUP_COMPONENT_NAME },
|
|
47
|
+
react_1.default.createElement(contexts_1.AccordionGroupContext.Provider, { value: singleExpanded ? groupName : null }, children)));
|
|
12
48
|
}
|
|
13
49
|
const AccordionGroupWrapper = styled_components_1.default.div `
|
|
14
50
|
margin: var(--md-accordion-group-margin);
|
|
@@ -45,6 +45,7 @@ const utils_1 = require("../../../core/utils");
|
|
|
45
45
|
const LinkIcon_1 = require("../../../icons/LinkIcon/LinkIcon");
|
|
46
46
|
const PageActions_1 = require("../../../components/PageActions/PageActions");
|
|
47
47
|
const hooks_1 = require("../../../core/hooks");
|
|
48
|
+
const contexts_1 = require("../../../core/contexts");
|
|
48
49
|
const constants_1 = require("../../../core/constants");
|
|
49
50
|
function renderWithSpanWrapper(children) {
|
|
50
51
|
const childrenArray = react_1.default.Children.toArray(children);
|
|
@@ -68,10 +69,12 @@ function renderWithSpanWrapper(children) {
|
|
|
68
69
|
}
|
|
69
70
|
function Heading({ level, id, children, 'data-source': dataSource, 'data-hash': dataHash, className, __idx, }) {
|
|
70
71
|
var _a, _b;
|
|
72
|
+
var _c;
|
|
71
73
|
const { pathname } = (0, react_router_1.useLocation)();
|
|
72
74
|
const { usePageProps } = (0, hooks_1.useThemeHooks)();
|
|
73
75
|
const pageProps = usePageProps();
|
|
74
|
-
|
|
76
|
+
// The page-type fallback is only for backward compatibility; templates set the scope.
|
|
77
|
+
const withPageActions = (_c = (0, react_1.useContext)(contexts_1.PageActionsContext)) !== null && _c !== void 0 ? _c : (((_a = pageProps === null || pageProps === void 0 ? void 0 : pageProps.metadata) === null || _a === void 0 ? void 0 : _a.type) === 'markdown' || ((_b = pageProps === null || pageProps === void 0 ? void 0 : pageProps.metadata) === null || _b === void 0 ? void 0 : _b.type) === 'asciidoc');
|
|
75
78
|
const headingTextId = `${id}-heading-text`;
|
|
76
79
|
const linkEl = (react_1.default.createElement("a", { "aria-labelledby": headingTextId, href: `#${id}`, className: (0, utils_1.concatClassNames)(constants_1.ANCHOR_CLASS, 'before') },
|
|
77
80
|
react_1.default.createElement(LinkIcon_1.LinkIcon, { "aria-hidden": "true", focusable: "false", color: "--heading-anchor-color" })));
|
|
@@ -80,7 +83,7 @@ function Heading({ level, id, children, 'data-source': dataSource, 'data-hash':
|
|
|
80
83
|
react_1.default.createElement(HeadingContentWrapper, null,
|
|
81
84
|
linkEl,
|
|
82
85
|
headingText,
|
|
83
|
-
|
|
86
|
+
withPageActions && __idx === 0 ? react_1.default.createElement(PageActions_1.PageActions, { pageSlug: pathname }) : null)));
|
|
84
87
|
}
|
|
85
88
|
const HeadingContentWrapper = styled_components_1.default.div `
|
|
86
89
|
display: flex;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/theme",
|
|
3
|
-
"version": "0.69.0
|
|
3
|
+
"version": "0.69.0",
|
|
4
4
|
"description": "Shared UI components lib",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"theme",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@babel/core": "7.29.7",
|
|
44
44
|
"@markdoc/markdoc": "0.5.2",
|
|
45
|
-
"@redocly/realm-asyncapi-sdk": "0.15.0
|
|
45
|
+
"@redocly/realm-asyncapi-sdk": "0.15.0",
|
|
46
46
|
"@shikijs/types": "3.21.0",
|
|
47
47
|
"@testing-library/react": "16.3.0",
|
|
48
48
|
"@testing-library/user-event": "14.6.1",
|
|
@@ -88,6 +88,7 @@ export function CatalogEntities(props: CatalogEntitiesProps): JSX.Element {
|
|
|
88
88
|
(query.isPlaceholderData && entities && entities.length >= LOAD_MORE_THRESHOLD);
|
|
89
89
|
|
|
90
90
|
useEffect(() => {
|
|
91
|
+
// oxlint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent -- the entities query lives in this ejectable component; the fetched total is mirrored up so the portal page header can show the count
|
|
91
92
|
setEntitiesCounter(total || 0);
|
|
92
93
|
}, [total, setEntitiesCounter]);
|
|
93
94
|
|
|
@@ -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
|
/**
|
|
@@ -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]);
|
|
@@ -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
|
})),
|
|
@@ -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) => {
|
|
@@ -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(
|
|
@@ -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: () => {},
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const SPACE_DELIMITER = ' ';
|
|
2
2
|
|
|
3
|
+
// EasyList/AdGuard ship generic cosmetic rules that hide these exact class tokens on every
|
|
4
|
+
// site; the trailing underscore keeps the class usable without matching the filter lists.
|
|
5
|
+
const ADBLOCK_HIDDEN_CLASS_NAMES = new Set(['product-ad', 'product-ads', 'product-inlist-ad']);
|
|
6
|
+
|
|
7
|
+
export function getSafeClassName(className: string): string {
|
|
8
|
+
return ADBLOCK_HIDDEN_CLASS_NAMES.has(className) ? `${className}_` : className;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
export function concatClassNames(...args: Array<string | undefined>): string {
|
|
4
12
|
return args
|
|
5
13
|
.map((className) => (className ? className.trim() : className))
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
2
|
import styled, { keyframes } from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX, PropsWithChildren } from 'react';
|
|
5
5
|
|
|
6
6
|
import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronRightIcon';
|
|
7
7
|
import { ACCORDION_COMPONENT_NAME } from '@redocly/theme/core/constants';
|
|
8
|
+
import { AccordionGroupContext } from '@redocly/theme/core/contexts';
|
|
8
9
|
|
|
9
10
|
export type AccordionProps = {
|
|
10
11
|
title: string;
|
|
@@ -22,10 +23,13 @@ export function Accordion({
|
|
|
22
23
|
'data-source': dataSource,
|
|
23
24
|
'data-hash': dataHash,
|
|
24
25
|
}: PropsWithChildren<AccordionProps>): JSX.Element {
|
|
26
|
+
const groupName = useContext(AccordionGroupContext);
|
|
27
|
+
|
|
25
28
|
return (
|
|
26
29
|
<AccordionWrapper
|
|
27
30
|
className={className}
|
|
28
31
|
open={expanded}
|
|
32
|
+
name={groupName ?? undefined}
|
|
29
33
|
data-component-name={ACCORDION_COMPONENT_NAME}
|
|
30
34
|
data-source={dataSource}
|
|
31
35
|
data-hash={dataHash}
|
|
@@ -37,7 +41,9 @@ export function Accordion({
|
|
|
37
41
|
/>
|
|
38
42
|
{title}
|
|
39
43
|
</AccordionSummary>
|
|
40
|
-
<AccordionBody>
|
|
44
|
+
<AccordionBody>
|
|
45
|
+
<AccordionGroupContext.Provider value={null}>{children}</AccordionGroupContext.Provider>
|
|
46
|
+
</AccordionBody>
|
|
41
47
|
</AccordionWrapper>
|
|
42
48
|
);
|
|
43
49
|
}
|
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import type { JSX, PropsWithChildren } from 'react';
|
|
5
5
|
|
|
6
6
|
import { ACCORDION_GROUP_COMPONENT_NAME, ACCORDION_SELECTOR } from '@redocly/theme/core/constants';
|
|
7
|
+
import { AccordionGroupContext } from '@redocly/theme/core/contexts';
|
|
7
8
|
|
|
8
9
|
export type AccordionGroupProps = {
|
|
10
|
+
singleExpanded?: boolean;
|
|
9
11
|
className?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export function AccordionGroup({
|
|
15
|
+
singleExpanded = false,
|
|
13
16
|
className,
|
|
14
17
|
children,
|
|
15
18
|
}: PropsWithChildren<AccordionGroupProps>): JSX.Element {
|
|
19
|
+
const groupName = useId();
|
|
20
|
+
|
|
16
21
|
return (
|
|
17
22
|
<AccordionGroupWrapper
|
|
18
23
|
className={className}
|
|
19
24
|
data-component-name={ACCORDION_GROUP_COMPONENT_NAME}
|
|
20
25
|
>
|
|
21
|
-
{
|
|
26
|
+
<AccordionGroupContext.Provider value={singleExpanded ? groupName : null}>
|
|
27
|
+
{children}
|
|
28
|
+
</AccordionGroupContext.Provider>
|
|
22
29
|
</AccordionGroupWrapper>
|
|
23
30
|
);
|
|
24
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createElement, isValidElement } from 'react';
|
|
1
|
+
import React, { createElement, isValidElement, useContext } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { useLocation } from 'react-router';
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ import { concatClassNames } from '@redocly/theme/core/utils';
|
|
|
9
9
|
import { LinkIcon } from '@redocly/theme/icons/LinkIcon/LinkIcon';
|
|
10
10
|
import { PageActions } from '@redocly/theme/components/PageActions/PageActions';
|
|
11
11
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
12
|
+
import { PageActionsContext } from '@redocly/theme/core/contexts';
|
|
12
13
|
import {
|
|
13
14
|
ANCHOR_CLASS,
|
|
14
15
|
HEADING_ANCHOR_CLASS,
|
|
@@ -65,8 +66,10 @@ export function Heading({
|
|
|
65
66
|
const { usePageProps } = useThemeHooks();
|
|
66
67
|
const pageProps = usePageProps();
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// The page-type fallback is only for backward compatibility; templates set the scope.
|
|
70
|
+
const withPageActions =
|
|
71
|
+
useContext(PageActionsContext) ??
|
|
72
|
+
(pageProps?.metadata?.type === 'markdown' || pageProps?.metadata?.type === 'asciidoc');
|
|
70
73
|
const headingTextId = `${id}-heading-text`;
|
|
71
74
|
|
|
72
75
|
const linkEl = (
|
|
@@ -101,7 +104,7 @@ export function Heading({
|
|
|
101
104
|
<HeadingContentWrapper>
|
|
102
105
|
{linkEl}
|
|
103
106
|
{headingText}
|
|
104
|
-
{
|
|
107
|
+
{withPageActions && __idx === 0 ? <PageActions pageSlug={pathname} /> : null}
|
|
105
108
|
</HeadingContentWrapper>
|
|
106
109
|
</div>
|
|
107
110
|
);
|
|
@@ -3,6 +3,9 @@ import type { MarkdocSchemaWrapper } from '@redocly/theme/markdoc/tags/types';
|
|
|
3
3
|
export const accordionGroup: MarkdocSchemaWrapper = {
|
|
4
4
|
schema: {
|
|
5
5
|
render: 'AccordionGroup',
|
|
6
|
+
attributes: {
|
|
7
|
+
singleExpanded: { type: Boolean, default: false },
|
|
8
|
+
},
|
|
6
9
|
},
|
|
7
10
|
tagName: 'accordion-group',
|
|
8
11
|
};
|