@redocly/theme 0.69.0-next.1 → 0.69.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/Badge/Badge.d.ts +3 -1
- package/lib/components/Badge/Badge.js +7 -2
- package/lib/components/Buttons/ConnectMCPButton.js +43 -14
- package/lib/components/MCP/McpConnectionTabs.d.ts +7 -0
- package/lib/components/MCP/McpConnectionTabs.js +151 -0
- package/lib/core/constants/mcp.d.ts +2 -0
- package/lib/core/constants/mcp.js +8 -1
- package/lib/core/hooks/use-active-heading.js +59 -15
- package/lib/core/hooks/use-active-section-id.js +3 -1
- package/lib/core/hooks/use-connect-mcp-button.d.ts +1 -1
- package/lib/core/hooks/use-connect-mcp-button.js +16 -13
- package/lib/core/styles/global.js +6 -1
- package/lib/core/types/catalog.d.ts +2 -0
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/core/types/mcp.d.ts +1 -1
- package/lib/core/utils/mcp.d.ts +5 -0
- package/lib/core/utils/mcp.js +35 -0
- package/lib/icons/CodexIcon/CodexIcon.d.ts +25 -0
- package/lib/icons/CodexIcon/CodexIcon.js +22 -0
- package/lib/layouts/McpNotAllowed.js +5 -3
- package/lib/markdoc/components/Cards/Card.d.ts +8 -1
- package/lib/markdoc/components/Cards/Card.js +55 -16
- package/lib/markdoc/components/Cards/CardBadge.d.ts +9 -0
- package/lib/markdoc/components/Cards/CardBadge.js +30 -0
- package/lib/markdoc/components/Cards/CardIcon.d.ts +5 -4
- package/lib/markdoc/components/Cards/CardIcon.js +6 -3
- package/lib/markdoc/components/Cards/variables.dark.js +32 -0
- package/lib/markdoc/components/Cards/variables.js +76 -4
- package/lib/markdoc/tags/card.js +14 -0
- package/lib/markdoc/tags/connect-mcp.js +2 -1
- package/package.json +2 -2
- package/src/components/Badge/Badge.tsx +16 -2
- package/src/components/Buttons/ConnectMCPButton.tsx +44 -14
- package/src/components/MCP/McpConnectionTabs.tsx +263 -0
- package/src/core/constants/mcp.ts +10 -0
- package/src/core/hooks/use-active-heading.ts +79 -15
- package/src/core/hooks/use-active-section-id.ts +3 -1
- package/src/core/hooks/use-connect-mcp-button.ts +23 -15
- package/src/core/styles/global.ts +6 -1
- package/src/core/types/catalog.ts +3 -0
- package/src/core/types/l10n.ts +17 -0
- package/src/core/types/mcp.ts +1 -1
- package/src/core/utils/mcp.ts +39 -0
- package/src/icons/CodexIcon/CodexIcon.tsx +23 -0
- package/src/layouts/McpNotAllowed.tsx +4 -2
- package/src/markdoc/components/Cards/Card.tsx +79 -14
- package/src/markdoc/components/Cards/CardBadge.tsx +39 -0
- package/src/markdoc/components/Cards/CardIcon.tsx +16 -5
- package/src/markdoc/components/Cards/variables.dark.ts +32 -0
- package/src/markdoc/components/Cards/variables.ts +76 -4
- package/src/markdoc/tags/card.ts +14 -0
- package/src/markdoc/tags/connect-mcp.ts +3 -1
|
@@ -6,13 +6,15 @@ import type { MCPOption } from '../types';
|
|
|
6
6
|
import { useThemeHooks } from './use-theme-hooks';
|
|
7
7
|
import { useMCPConfig } from './use-mcp-config';
|
|
8
8
|
import { ClipboardService } from '../utils/clipboard-service';
|
|
9
|
+
import { DEFAULT_MCP_OPTIONS } from '../constants/mcp';
|
|
10
|
+
import { generateClaudeCodeCommand, generateCodexCommand } from '../utils/mcp';
|
|
9
11
|
|
|
10
12
|
export type McpButtonOptions = {
|
|
11
13
|
options?: MCPOption[];
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export type McpButtonSettings = {
|
|
15
|
-
|
|
17
|
+
copiedOption: MCPOption | null;
|
|
16
18
|
cursorUrl: string;
|
|
17
19
|
vscodeUrl: string;
|
|
18
20
|
triggerButtonText: string;
|
|
@@ -23,35 +25,41 @@ export type McpButtonSettings = {
|
|
|
23
25
|
const COPIED_RESET_TIMEOUT = 1000;
|
|
24
26
|
|
|
25
27
|
export function useConnectMCPButton({
|
|
26
|
-
options =
|
|
28
|
+
options = DEFAULT_MCP_OPTIONS,
|
|
27
29
|
}: McpButtonOptions = {}): McpButtonSettings {
|
|
28
30
|
const { useTranslate } = useThemeHooks();
|
|
29
31
|
const { translate } = useTranslate();
|
|
30
32
|
const { serverName, serverUrl, cursorUrl, vscodeUrl } = useMCPConfig();
|
|
31
|
-
const [
|
|
33
|
+
const [copiedOption, setCopiedOption] = useState<MCPOption | null>(null);
|
|
32
34
|
|
|
33
35
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
36
|
const resetCopied = useCallback(
|
|
35
|
-
debounce(() =>
|
|
37
|
+
debounce(() => setCopiedOption(null), COPIED_RESET_TIMEOUT),
|
|
36
38
|
[],
|
|
37
39
|
);
|
|
38
40
|
|
|
39
41
|
const handleAction = useCallback(
|
|
40
42
|
(action: MCPOption) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
const connection = { serverName, url: serverUrl };
|
|
44
|
+
const copyTextMap: Partial<Record<MCPOption, string>> = {
|
|
45
|
+
copy: JSON.stringify(
|
|
46
|
+
{ [serverName]: { url: serverUrl, description: 'MCP Server' } },
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
),
|
|
50
|
+
'claude-code': generateClaudeCodeCommand(connection),
|
|
51
|
+
codex: generateCodexCommand(connection),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const copyText = copyTextMap[action];
|
|
55
|
+
if (copyText !== undefined) {
|
|
56
|
+
ClipboardService.copyCustom(copyText);
|
|
57
|
+
setCopiedOption(action);
|
|
50
58
|
resetCopied();
|
|
51
59
|
return;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
const urlMap: Record<
|
|
62
|
+
const urlMap: Partial<Record<MCPOption, string>> = {
|
|
55
63
|
cursor: cursorUrl,
|
|
56
64
|
vscode: vscodeUrl,
|
|
57
65
|
};
|
|
@@ -69,7 +77,7 @@ export function useConnectMCPButton({
|
|
|
69
77
|
const visibleOptions = useMemo(() => options.filter(Boolean), [options]);
|
|
70
78
|
|
|
71
79
|
return {
|
|
72
|
-
|
|
80
|
+
copiedOption,
|
|
73
81
|
cursorUrl,
|
|
74
82
|
vscodeUrl,
|
|
75
83
|
triggerButtonText,
|
|
@@ -1050,8 +1050,8 @@ const pages = css`
|
|
|
1050
1050
|
|
|
1051
1051
|
--page-mcp-405-status-text-color: var(--text-color-helper);
|
|
1052
1052
|
--page-mcp-405-status-font-size: var(--font-size-lg); // @presenter FontSize
|
|
1053
|
-
--page-mcp-405-status-line-height: var(--line-height-lg); // @presenter LineHeight
|
|
1054
1053
|
--page-mcp-405-status-font-weight: var(--font-weight-semibold); // @presenter FontWeight
|
|
1054
|
+
--page-mcp-405-status-line-height: var(--line-height-lg); // @presenter LineHeight
|
|
1055
1055
|
|
|
1056
1056
|
--page-mcp-405-title-text-color: var(--text-color-primary);
|
|
1057
1057
|
--page-mcp-405-title-font-size: 42px; // @presenter FontSize
|
|
@@ -1067,6 +1067,11 @@ const pages = css`
|
|
|
1067
1067
|
--page-mcp-405-description-font-size-mobile: var(--font-size-lg);
|
|
1068
1068
|
--page-mcp-405-description-line-height-mobile: var(--line-height-lg);
|
|
1069
1069
|
|
|
1070
|
+
--page-mcp-405-snippets-font-size: var(--font-size-base); // @presenter FontSize
|
|
1071
|
+
--page-mcp-405-snippets-line-height: var(--line-height-base); // @presenter LineHeight
|
|
1072
|
+
--page-mcp-405-snippets-hint-text-color: var(--text-color-secondary);
|
|
1073
|
+
--page-mcp-405-snippets-code-padding-right: var(--spacing-xxl);
|
|
1074
|
+
|
|
1070
1075
|
// @tokens End
|
|
1071
1076
|
|
|
1072
1077
|
/**
|
|
@@ -212,6 +212,9 @@ export type BffCatalogEntityList = BffCatalogList<BffCatalogEntity>;
|
|
|
212
212
|
export type BffCatalogRelatedEntityList = BffCatalogList<BffCatalogRelatedEntity>;
|
|
213
213
|
|
|
214
214
|
export type Page = {
|
|
215
|
+
// Optional so existing code that builds a Page keeps compiling; the catalog BFF always emits both.
|
|
216
|
+
nextPage?: string | null;
|
|
217
|
+
previousPage?: string | null;
|
|
215
218
|
endCursor: string | null;
|
|
216
219
|
startCursor: string | null;
|
|
217
220
|
hasNextPage: boolean;
|
package/src/core/types/l10n.ts
CHANGED
|
@@ -136,6 +136,19 @@ export type TranslationKey =
|
|
|
136
136
|
| 'page.notFound.description'
|
|
137
137
|
| 'page.mcpNotAllowed.title'
|
|
138
138
|
| 'page.mcpNotAllowed.description'
|
|
139
|
+
| 'page.mcpNotAllowed.snippets.terminal.hint'
|
|
140
|
+
| 'page.mcpNotAllowed.snippets.claudeCode.headline'
|
|
141
|
+
| 'page.mcpNotAllowed.snippets.claudeDesktop.headline'
|
|
142
|
+
| 'page.mcpNotAllowed.snippets.claudeDesktop.instruction'
|
|
143
|
+
| 'page.mcpNotAllowed.snippets.chatgptApp.headline'
|
|
144
|
+
| 'page.mcpNotAllowed.snippets.chatgptApp.instruction'
|
|
145
|
+
| 'page.mcpNotAllowed.snippets.codexCli.headline'
|
|
146
|
+
| 'page.mcpNotAllowed.snippets.prompt.headline'
|
|
147
|
+
| 'page.mcpNotAllowed.snippets.prompt.hint'
|
|
148
|
+
| 'page.mcpNotAllowed.snippets.json.hint'
|
|
149
|
+
| 'page.mcpNotAllowed.tabs.claude'
|
|
150
|
+
| 'page.mcpNotAllowed.tabs.chatgpt'
|
|
151
|
+
| 'page.mcpNotAllowed.tabs.json'
|
|
139
152
|
| 'page.lastUpdated.timeago'
|
|
140
153
|
| 'page.lastUpdated.on'
|
|
141
154
|
| 'catalog.filters.placeholder'
|
|
@@ -271,6 +284,10 @@ export type TranslationKey =
|
|
|
271
284
|
| 'page.actions.connectMcp.cursorDescription'
|
|
272
285
|
| 'page.actions.connectMcp.vscode'
|
|
273
286
|
| 'page.actions.connectMcp.vscodeDescription'
|
|
287
|
+
| 'page.actions.connectMcp.claudeCode'
|
|
288
|
+
| 'page.actions.connectMcp.claudeCodeDescription'
|
|
289
|
+
| 'page.actions.connectMcp.codex'
|
|
290
|
+
| 'page.actions.connectMcp.codexDescription'
|
|
274
291
|
| 'page.actions.connectMcp.copyConfig'
|
|
275
292
|
| 'page.actions.connectMcp.copyConfigDescription'
|
|
276
293
|
| 'openapi.download.description.title'
|
package/src/core/types/mcp.ts
CHANGED
package/src/core/utils/mcp.ts
CHANGED
|
@@ -32,3 +32,42 @@ export function generateMCPDeepLink(
|
|
|
32
32
|
return generateCursorMCPDeepLink(config);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Server names can contain spaces (e.g. "Docs MCP server") which break bare CLI
|
|
37
|
+
// arguments, so commands use a slugified form while JSON configs keep the raw name.
|
|
38
|
+
export function slugifyMcpServerName(name: string): string {
|
|
39
|
+
return name
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
42
|
+
.replace(/^-+|-+$/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Claude Desktop's config file only launches stdio commands, so remote servers
|
|
46
|
+
// connect through the mcp-remote bridge.
|
|
47
|
+
export function generateClaudeDesktopConfig(config: McpConnectionParams): string {
|
|
48
|
+
return JSON.stringify(
|
|
49
|
+
{
|
|
50
|
+
mcpServers: {
|
|
51
|
+
[slugifyMcpServerName(config.serverName)]: {
|
|
52
|
+
command: 'npx',
|
|
53
|
+
args: ['-y', 'mcp-remote', config.url],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateClaudeCodeCommand(config: McpConnectionParams): string {
|
|
63
|
+
return `claude mcp add --transport http ${slugifyMcpServerName(config.serverName)} ${config.url}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function generateCodexCommand(config: McpConnectionParams): string {
|
|
67
|
+
return `codex mcp add ${slugifyMcpServerName(config.serverName)} --url ${config.url}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pasted into an AI assistant (Claude Code, Codex) so it installs the server itself.
|
|
71
|
+
export function generateMcpInstallPrompt(config: McpConnectionParams): string {
|
|
72
|
+
return `Add the MCP server "${slugifyMcpServerName(config.serverName)}" with URL ${config.url} (streamable HTTP transport) to my MCP configuration, then verify it is connected.`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { IconProps } from '@redocly/theme/icons/types';
|
|
5
|
+
|
|
6
|
+
import { getCssColorVariable } from '@redocly/theme/core/utils';
|
|
7
|
+
|
|
8
|
+
const Icon = (props: IconProps) => (
|
|
9
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
10
|
+
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.073zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.8956zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997z" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const CodexIcon = styled(Icon).attrs({
|
|
15
|
+
'data-component-name': 'icons/CodexIcon/CodexIcon',
|
|
16
|
+
})`
|
|
17
|
+
path {
|
|
18
|
+
fill: ${({ color }) => getCssColorVariable(color)};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
height: ${({ size }) => size || '16px'};
|
|
22
|
+
width: ${({ size }) => size || '16px'};
|
|
23
|
+
`;
|
|
@@ -5,6 +5,7 @@ import type { JSX } from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
7
7
|
import { ConnectMCPButton } from '@redocly/theme/components/Buttons/ConnectMCPButton';
|
|
8
|
+
import { McpConnectionTabs } from '@redocly/theme/components/MCP/McpConnectionTabs';
|
|
8
9
|
|
|
9
10
|
export default function McpNotAllowed(): JSX.Element {
|
|
10
11
|
const { useTranslate } = useThemeHooks();
|
|
@@ -14,17 +15,18 @@ export default function McpNotAllowed(): JSX.Element {
|
|
|
14
15
|
<NotAllowedWrapper data-component-name="layouts/McpNotAllowed">
|
|
15
16
|
<StatusText>405</StatusText>
|
|
16
17
|
<Title data-translation-key="page.mcpNotAllowed.title">
|
|
17
|
-
{translate('page.mcpNotAllowed.title', '
|
|
18
|
+
{translate('page.mcpNotAllowed.title', 'Connect to the MCP server')}
|
|
18
19
|
</Title>
|
|
19
20
|
<Description data-translation-key="page.mcpNotAllowed.description">
|
|
20
21
|
{translate(
|
|
21
22
|
'page.mcpNotAllowed.description',
|
|
22
|
-
'To use this MCP server, register it in your MCP
|
|
23
|
+
'To use this MCP server, register it in your MCP client (for example: VS Code, Cursor, or Claude Code).',
|
|
23
24
|
)}
|
|
24
25
|
</Description>
|
|
25
26
|
<ButtonWrapper>
|
|
26
27
|
<ConnectMCPButton alignment="start" />
|
|
27
28
|
</ButtonWrapper>
|
|
29
|
+
<McpConnectionTabs />
|
|
28
30
|
</NotAllowedWrapper>
|
|
29
31
|
);
|
|
30
32
|
}
|
|
@@ -2,8 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
|
|
4
4
|
import { ChevronRightIcon } from '@redocly/theme/icons/ChevronRightIcon/ChevronRightIcon';
|
|
5
|
+
import { ArrowUpRightIcon } from '@redocly/theme/icons/ArrowUpRightIcon/ArrowUpRightIcon';
|
|
5
6
|
import { CardImage } from '@redocly/theme/markdoc/components/Cards/CardImage';
|
|
6
7
|
import { CardIcon, CardIconWrapper } from '@redocly/theme/markdoc/components/Cards/CardIcon';
|
|
8
|
+
import { CardBadge } from '@redocly/theme/markdoc/components/Cards/CardBadge';
|
|
7
9
|
import { Link } from '@redocly/theme/components/Link/Link';
|
|
8
10
|
|
|
9
11
|
export type CardProps = React.PropsWithChildren<{
|
|
@@ -17,8 +19,15 @@ export type CardProps = React.PropsWithChildren<{
|
|
|
17
19
|
layout?: 'horizontal' | 'vertical' | 'combined';
|
|
18
20
|
variant?: 'filled' | 'outlined' | 'elevated' | 'ghost';
|
|
19
21
|
iconVariant?: 'filled' | 'ghost';
|
|
22
|
+
iconColor?: string;
|
|
20
23
|
align?: 'start' | 'center' | 'end';
|
|
21
24
|
to?: string;
|
|
25
|
+
linkIcon?: 'chevron' | 'arrow';
|
|
26
|
+
cta?: React.ReactNode;
|
|
27
|
+
badge?: string;
|
|
28
|
+
badgeColor?: string;
|
|
29
|
+
badgeIcon?: string;
|
|
30
|
+
badgeIconRawContent?: string;
|
|
22
31
|
}>;
|
|
23
32
|
|
|
24
33
|
export function Card({
|
|
@@ -32,8 +41,15 @@ export function Card({
|
|
|
32
41
|
variant = 'filled',
|
|
33
42
|
lineClamp,
|
|
34
43
|
iconVariant = 'filled',
|
|
44
|
+
iconColor,
|
|
35
45
|
align = 'start',
|
|
36
46
|
to,
|
|
47
|
+
cta,
|
|
48
|
+
linkIcon,
|
|
49
|
+
badge,
|
|
50
|
+
badgeColor,
|
|
51
|
+
badgeIcon,
|
|
52
|
+
badgeIconRawContent,
|
|
37
53
|
children,
|
|
38
54
|
}: CardProps) {
|
|
39
55
|
const titleNoSpaces = title?.replace(/\s+/g, '-').toLowerCase();
|
|
@@ -66,14 +82,30 @@ export function Card({
|
|
|
66
82
|
src={icon}
|
|
67
83
|
rawContent={iconRawContent}
|
|
68
84
|
position={iconPosition}
|
|
85
|
+
color={iconColor}
|
|
69
86
|
/>
|
|
70
87
|
)}
|
|
71
88
|
<ContentWrapper>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
<TitleWrapper id={cardTitleId} $isCardLink={!!to} $justifyContent={justifyContent}>
|
|
90
|
+
<Title>
|
|
91
|
+
{title}
|
|
92
|
+
{!!to && !!linkIcon && (
|
|
93
|
+
<CardLinkIcon as={linkIcon === 'arrow' ? ArrowUpRightIcon : undefined} />
|
|
94
|
+
)}
|
|
95
|
+
</Title>
|
|
96
|
+
{!!badge && (
|
|
97
|
+
<CardBadge color={badgeColor} icon={badgeIcon} iconRawContent={badgeIconRawContent}>
|
|
98
|
+
{badge}
|
|
99
|
+
</CardBadge>
|
|
100
|
+
)}
|
|
101
|
+
</TitleWrapper>
|
|
76
102
|
<Body $lineClamp={lineClamp}>{children}</Body>
|
|
103
|
+
{!!to && !!cta && (
|
|
104
|
+
<Cta $justifyContent={justifyContent}>
|
|
105
|
+
<CtaText> {cta} </CtaText>
|
|
106
|
+
<CtaIcon />
|
|
107
|
+
</Cta>
|
|
108
|
+
)}
|
|
77
109
|
</ContentWrapper>
|
|
78
110
|
</CardContentWrapper>
|
|
79
111
|
</>
|
|
@@ -105,25 +137,28 @@ const ContentWrapper = styled.div`
|
|
|
105
137
|
width: 100%;
|
|
106
138
|
`;
|
|
107
139
|
|
|
108
|
-
const
|
|
109
|
-
margin: var(--card-title-margin);
|
|
110
|
-
font-weight: var(--card-title-font-weight);
|
|
140
|
+
const TitleWrapper = styled.div<{ $isCardLink: boolean; $justifyContent: string }>`
|
|
111
141
|
color: var(--card-title-text-color);
|
|
112
142
|
display: flex;
|
|
113
143
|
align-items: center;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
row-gap: var(--card-title-row-gap);
|
|
114
146
|
justify-content: ${({ $justifyContent }) => $justifyContent};
|
|
115
147
|
width: 100%;
|
|
116
148
|
`;
|
|
117
149
|
|
|
150
|
+
const Title = styled.h3`
|
|
151
|
+
margin: var(--card-title-margin);
|
|
152
|
+
font-weight: var(--card-title-font-weight);
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
// The `as` prop selects the icon; the chevron is the default target.
|
|
118
156
|
const CardLinkIcon = styled(ChevronRightIcon)`
|
|
119
157
|
width: var(--card-link-icon-width);
|
|
120
158
|
height: var(--card-link-icon-height);
|
|
121
|
-
color: var(--
|
|
159
|
+
color: var(--icon-color-secondary);
|
|
122
160
|
margin-left: var(--spacing-xxs);
|
|
123
|
-
|
|
124
|
-
transition:
|
|
125
|
-
opacity 0.3s,
|
|
126
|
-
transform 0.3s;
|
|
161
|
+
margin-top: calc(var(--spacing-xxs) / 2);
|
|
127
162
|
`;
|
|
128
163
|
|
|
129
164
|
const Body = styled.div<{ $lineClamp?: number }>`
|
|
@@ -149,6 +184,29 @@ const Body = styled.div<{ $lineClamp?: number }>`
|
|
|
149
184
|
}
|
|
150
185
|
`;
|
|
151
186
|
|
|
187
|
+
const Cta = styled.div<{ $justifyContent: string }>`
|
|
188
|
+
display: flex;
|
|
189
|
+
gap: var(--card-cta-gap);
|
|
190
|
+
align-items: center;
|
|
191
|
+
margin-top: var(--card-cta-margin-top);
|
|
192
|
+
color: var(--card-cta-text-color);
|
|
193
|
+
font-size: var(--card-cta-font-size);
|
|
194
|
+
justify-content: ${({ $justifyContent }) => $justifyContent};
|
|
195
|
+
|
|
196
|
+
&& > * {
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const CtaText = styled.span`
|
|
202
|
+
font-weight: var(--card-cta-font-weight);
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
const CtaIcon = styled(ChevronRightIcon)`
|
|
206
|
+
width: var(--card-cta-icon-width);
|
|
207
|
+
height: var(--card-cta-icon-height);
|
|
208
|
+
`;
|
|
209
|
+
|
|
152
210
|
const CardWrapper = styled.div.attrs<{
|
|
153
211
|
$variant: string;
|
|
154
212
|
$isCardLink: boolean;
|
|
@@ -185,6 +243,13 @@ const CardWrapper = styled.div.attrs<{
|
|
|
185
243
|
height: 100%;
|
|
186
244
|
object-fit: cover;
|
|
187
245
|
`}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&:hover,
|
|
249
|
+
&:focus-within {
|
|
250
|
+
${Cta} {
|
|
251
|
+
color: var(--card-link-icon-color);
|
|
252
|
+
}
|
|
188
253
|
}
|
|
189
254
|
|
|
190
255
|
&.card-link {
|
|
@@ -203,8 +268,8 @@ const CardWrapper = styled.div.attrs<{
|
|
|
203
268
|
background-color: var(--card-icon-bg-color-hover);
|
|
204
269
|
}
|
|
205
270
|
|
|
206
|
-
|
|
207
|
-
|
|
271
|
+
${CardLinkIcon} {
|
|
272
|
+
color: var(--card-link-icon-color);
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
275
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { Badge } from '@redocly/theme/components/Badge/Badge';
|
|
5
|
+
|
|
6
|
+
export type CardBadgeProps = React.PropsWithChildren<{
|
|
7
|
+
color?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
/** Inline SVG content for the icon */
|
|
10
|
+
iconRawContent?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export function CardBadge({ color, icon, iconRawContent, className, children }: CardBadgeProps) {
|
|
15
|
+
// `tag-default` keeps a badge grey when the name is absent or unknown.
|
|
16
|
+
const classNames = ['tag-default', color && `tag-${color}`, className].filter(Boolean).join(' ');
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<CardBadgeWrapper className={classNames} icon={icon} iconRawContent={iconRawContent}>
|
|
20
|
+
{children}
|
|
21
|
+
</CardBadgeWrapper>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CardBadgeWrapper = styled(Badge)`
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
margin: var(--card-badge-margin);
|
|
29
|
+
padding: var(--card-badge-padding);
|
|
30
|
+
border-radius: var(--card-badge-border-radius);
|
|
31
|
+
font-size: var(--card-badge-font-size);
|
|
32
|
+
font-weight: var(--card-badge-font-weight);
|
|
33
|
+
line-height: var(--card-badge-line-height);
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
|
|
37
|
+
--badge-text-color: var(--tag-color);
|
|
38
|
+
--badge-bg-color: var(--tag-bg-color);
|
|
39
|
+
`;
|
|
@@ -4,24 +4,35 @@ import styled from 'styled-components';
|
|
|
4
4
|
import { GenericIcon } from '@redocly/theme/icons/GenericIcon/GenericIcon';
|
|
5
5
|
|
|
6
6
|
export type CardIconProps = {
|
|
7
|
-
variant?:
|
|
7
|
+
variant?: 'filled' | 'ghost';
|
|
8
8
|
src: string;
|
|
9
9
|
rawContent?: string;
|
|
10
10
|
position?: 'auto' | 'start' | 'center' | 'end';
|
|
11
|
+
color?: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export function CardIcon({ variant, src, rawContent, position }: CardIconProps) {
|
|
14
|
+
export function CardIcon({ variant, src, rawContent, position, color }: CardIconProps) {
|
|
14
15
|
return (
|
|
15
|
-
<CardIconWrapper
|
|
16
|
+
<CardIconWrapper
|
|
17
|
+
className={color && `card-icon-color-${color}`}
|
|
18
|
+
$variant={variant}
|
|
19
|
+
$position={position}
|
|
20
|
+
>
|
|
16
21
|
<GenericIcon icon={src} rawContent={rawContent} />
|
|
17
22
|
</CardIconWrapper>
|
|
18
23
|
);
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
export const CardIconWrapper = styled.div<{
|
|
26
|
+
export const CardIconWrapper = styled.div<{
|
|
27
|
+
$variant?: 'filled' | 'ghost';
|
|
28
|
+
$position?: string;
|
|
29
|
+
}>`
|
|
30
|
+
--card-icon-bg-color-hover: ${({ $variant }) => $variant === 'ghost' && 'transparent'};
|
|
22
31
|
--icon-width: var(--card-icon-width);
|
|
23
32
|
--icon-height: var(--card-icon-height);
|
|
24
33
|
color: var(--card-icon-color);
|
|
34
|
+
/* An inline SVG fills from this. */
|
|
35
|
+
--icon-color: var(--card-icon-color);
|
|
25
36
|
|
|
26
37
|
display: flex;
|
|
27
38
|
align-self: ${({ $position }) => ($position ? $position : 'auto')};
|
|
@@ -30,7 +41,7 @@ export const CardIconWrapper = styled.div<{ $variant?: string; $position?: strin
|
|
|
30
41
|
min-height: var(--card-icon-height);
|
|
31
42
|
flex-shrink: 0;
|
|
32
43
|
|
|
33
|
-
padding: var(--card-icon-padding);
|
|
44
|
+
padding: ${({ $variant }) => ($variant === 'ghost' ? 0 : `var(--card-icon-padding)`)};
|
|
34
45
|
border: ${({ $variant }) => $variant !== 'ghost' && `var(--card-icon-border)`};
|
|
35
46
|
border-radius: var(--card-icon-border-radius);
|
|
36
47
|
overflow: hidden;
|
|
@@ -21,5 +21,37 @@ export const cardsDarkMode = css`
|
|
|
21
21
|
--card-box-shadow: 0px 8px 24px -4px #00000014, 0px 6px 12px -6px #0000001F;
|
|
22
22
|
--card-box-shadow-hover: 0px 14px 64px -4px #0000001F, 0px 8px 22px -6px #0000001F;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
.card-icon-color-green {
|
|
26
|
+
--card-icon-color: var(--color-green-7);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.card-icon-color-grey {
|
|
30
|
+
--card-icon-color: var(--color-warm-grey-8);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.card-icon-color-turquoise {
|
|
34
|
+
--card-icon-color: var(--color-turquoise-6);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.card-icon-color-magenta {
|
|
38
|
+
--card-icon-color: var(--color-magenta-4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.card-icon-color-carrot {
|
|
42
|
+
--card-icon-color: var(--color-carrot-6);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.card-icon-color-grass {
|
|
46
|
+
--card-icon-color: var(--color-grass-7);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.card-icon-color-persian-green {
|
|
50
|
+
--card-icon-color: var(--color-persian-green-7);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.card-icon-color-sky {
|
|
54
|
+
--card-icon-color: var(--color-sky-6);
|
|
55
|
+
}
|
|
24
56
|
`;
|
|
25
57
|
/* eslint-enable theme/no-raw-colors-in-styles */
|
|
@@ -21,8 +21,8 @@ export const cards = css`
|
|
|
21
21
|
--card-border-radius: var(--border-radius-xl);
|
|
22
22
|
--card-border-style: solid;
|
|
23
23
|
--card-link-cursor: pointer;
|
|
24
|
-
--card-link-icon-width:
|
|
25
|
-
--card-link-icon-height:
|
|
24
|
+
--card-link-icon-width: 14px;
|
|
25
|
+
--card-link-icon-height: 14px;
|
|
26
26
|
--card-border-width: 1px;
|
|
27
27
|
--card-image-border-radius: var(--border-radius-md);
|
|
28
28
|
--card-image-aspect-ratio: 16 / 9;
|
|
@@ -33,12 +33,26 @@ export const cards = css`
|
|
|
33
33
|
--card-icon-height: var(--card-icon-size);
|
|
34
34
|
--card-icon-width: var(--card-icon-size);
|
|
35
35
|
--card-icon-gap: var(--spacing-base);
|
|
36
|
-
--card-title-margin: 0;
|
|
36
|
+
--card-title-margin: 0 var(--spacing-xs) 0 0;
|
|
37
37
|
--card-title-font-weight: var(--font-weight-semibold);
|
|
38
|
+
--card-title-row-gap: var(--spacing-xxs);
|
|
38
39
|
--card-content-gap: 4px;
|
|
39
40
|
--card-padding: 16px 20px;
|
|
40
41
|
--card-body-font-size: var(--font-size-base);
|
|
41
42
|
--card-body-line-height: var(--line-height-base);
|
|
43
|
+
--card-cta-margin-top: var(--spacing-base);
|
|
44
|
+
--card-cta-gap: var(--spacing-xxs);
|
|
45
|
+
--card-cta-font-size: var(--font-size-base);
|
|
46
|
+
--card-cta-font-weight: var(--font-weight-regular);
|
|
47
|
+
--card-cta-text-color: var(--text-color-secondary);
|
|
48
|
+
--card-cta-icon-width: 14px;
|
|
49
|
+
--card-cta-icon-height: 14px;
|
|
50
|
+
--card-badge-margin: 0 var(--spacing-xxs) 0 0 ;
|
|
51
|
+
--card-badge-padding: 0 var(--spacing-xxs);
|
|
52
|
+
--card-badge-border-radius: var(--border-radius);
|
|
53
|
+
--card-badge-font-size: var(--font-size-sm);
|
|
54
|
+
--card-badge-font-weight: var(--font-weight-regular);
|
|
55
|
+
--card-badge-line-height: var(--line-height-sm);
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
58
|
* @tokens Card variants
|
|
@@ -82,5 +96,63 @@ export const cards = css`
|
|
|
82
96
|
--card-hover-transition: initial;
|
|
83
97
|
}
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* @tokens Card icon colors
|
|
101
|
+
*/
|
|
102
|
+
.card-icon-color-red {
|
|
103
|
+
--card-icon-color: var(--color-red-7);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.card-icon-color-green {
|
|
107
|
+
--card-icon-color: var(--color-green-8);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.card-icon-color-blue {
|
|
111
|
+
--card-icon-color: var(--color-blue-7);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.card-icon-color-grey {
|
|
115
|
+
--card-icon-color: var(--text-color-secondary);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.card-icon-color-turquoise {
|
|
119
|
+
--card-icon-color: var(--color-turquoise-8);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.card-icon-color-magenta {
|
|
123
|
+
--card-icon-color: var(--color-magenta-7);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.card-icon-color-purple {
|
|
127
|
+
--card-icon-color: var(--color-purple-7);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.card-icon-color-carrot {
|
|
131
|
+
--card-icon-color: var(--color-carrot-8);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.card-icon-color-raspberry {
|
|
135
|
+
--card-icon-color: var(--color-raspberry-7);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.card-icon-color-orange {
|
|
139
|
+
--card-icon-color: var(--color-orange-7);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.card-icon-color-grass {
|
|
143
|
+
--card-icon-color: var(--color-grass-8);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.card-icon-color-persian-green {
|
|
147
|
+
--card-icon-color: var(--color-persian-green-8);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.card-icon-color-sky {
|
|
151
|
+
--card-icon-color: var(--color-sky-8);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.card-icon-color-blueberry {
|
|
155
|
+
--card-icon-color: var(--color-blueberry-7);
|
|
156
|
+
}
|
|
85
157
|
`;
|
|
86
|
-
/* eslint-enable theme/no-raw-colors-in-styles */
|
|
158
|
+
/* eslint-enable theme/no-raw-colors-in-styles */
|
package/src/markdoc/tags/card.ts
CHANGED
|
@@ -52,7 +52,21 @@ export const card: MarkdocSchemaWrapper = {
|
|
|
52
52
|
type: String,
|
|
53
53
|
matches: ['filled', 'ghost'],
|
|
54
54
|
},
|
|
55
|
+
iconColor: { type: String },
|
|
55
56
|
to: { type: String, resolver: 'link' },
|
|
57
|
+
linkIcon: {
|
|
58
|
+
type: String,
|
|
59
|
+
matches: ['arrow', 'chevron'],
|
|
60
|
+
},
|
|
61
|
+
cta: { type: String },
|
|
62
|
+
badge: { type: String },
|
|
63
|
+
badgeColor: { type: String },
|
|
64
|
+
badgeIcon: { type: String, resolver: 'relativeOrCdnIcon' },
|
|
65
|
+
badgeIconRawContent: {
|
|
66
|
+
// internal use only
|
|
67
|
+
type: String,
|
|
68
|
+
render: true,
|
|
69
|
+
},
|
|
56
70
|
},
|
|
57
71
|
},
|
|
58
72
|
tagName: 'card',
|