@marlinjai/clearify 1.5.2 → 1.6.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/dist/node/{chunk-NY3VVUQ3.js → chunk-7IXPTIHW.js} +1 -1
- package/dist/node/{chunk-UOVBAJMR.js → chunk-7VYQMIW2.js} +37 -47
- package/dist/node/node/build.js +2 -2
- package/dist/node/node/index.js +2 -2
- package/dist/node/vite-plugin/index.js +1 -1
- package/package.json +2 -2
- package/src/theme/Sidebar.tsx +135 -5
- package/src/theme/components/OpenAPI.tsx +42 -135
- package/src/theme/components/OpenAPIPage.tsx +94 -4
- package/src/theme/components/openapi/ApiHeader.tsx +119 -0
- package/src/theme/components/openapi/CodeExamples.tsx +100 -0
- package/src/theme/components/openapi/MethodBadge.tsx +48 -0
- package/src/theme/components/openapi/OperationCard.tsx +198 -0
- package/src/theme/components/openapi/ParameterTable.tsx +134 -0
- package/src/theme/components/openapi/ResponseList.tsx +148 -0
- package/src/theme/components/openapi/SchemaViewer.tsx +377 -0
- package/src/theme/components/openapi/TagGroup.tsx +105 -0
- package/src/theme/components/openapi/generate-snippets.ts +102 -0
- package/src/theme/components/openapi/index.ts +8 -0
- package/src/theme/styles/globals.css +5 -0
|
@@ -1,14 +1,104 @@
|
|
|
1
|
-
import { OpenAPI } from './OpenAPI.js';
|
|
2
1
|
// @ts-expect-error virtual module
|
|
3
2
|
import config from 'virtual:clearify/config';
|
|
4
3
|
// @ts-expect-error virtual module
|
|
5
4
|
import openapiSpec from 'virtual:clearify/openapi-spec';
|
|
6
5
|
|
|
6
|
+
import React, { useEffect } from 'react';
|
|
7
|
+
import { ApiHeader } from './openapi/ApiHeader.js';
|
|
8
|
+
import { TagGroup } from './openapi/TagGroup.js';
|
|
9
|
+
|
|
10
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
|
11
|
+
|
|
7
12
|
export default function OpenAPIPage() {
|
|
8
|
-
const
|
|
13
|
+
const spec = openapiSpec as Record<string, any> | null;
|
|
14
|
+
|
|
15
|
+
// Scroll to hash anchor on mount and hash change
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const scrollToHash = () => {
|
|
18
|
+
const hash = window.location.hash.slice(1); // remove #
|
|
19
|
+
if (!hash) return;
|
|
20
|
+
// Small delay to ensure DOM is rendered
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
const el = document.getElementById(hash);
|
|
23
|
+
if (el) {
|
|
24
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
25
|
+
}
|
|
26
|
+
}, 100);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
scrollToHash();
|
|
30
|
+
window.addEventListener('hashchange', scrollToHash);
|
|
31
|
+
return () => window.removeEventListener('hashchange', scrollToHash);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (!spec) {
|
|
35
|
+
return (
|
|
36
|
+
<div style={{
|
|
37
|
+
border: '1px solid var(--clearify-border)',
|
|
38
|
+
borderRadius: 'var(--clearify-radius)',
|
|
39
|
+
padding: '2rem',
|
|
40
|
+
textAlign: 'center',
|
|
41
|
+
color: 'var(--clearify-text-secondary)',
|
|
42
|
+
fontSize: '0.875rem',
|
|
43
|
+
}}>
|
|
44
|
+
No OpenAPI spec provided. Set <code style={{ fontFamily: 'var(--font-mono)' }}>openapi.spec</code> in your Clearify config.
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Determine base URL from spec or config
|
|
50
|
+
const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
|
|
51
|
+
|
|
52
|
+
// Group operations by tag
|
|
53
|
+
const paths = spec.paths ?? {};
|
|
54
|
+
const tagMap = new Map<string, { tag: string; description?: string; operations: Array<{ method: string; path: string; operation: Record<string, any> }> }>();
|
|
55
|
+
const tagDescriptions = new Map<string, string>();
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(spec.tags)) {
|
|
58
|
+
for (const t of spec.tags) {
|
|
59
|
+
if (t.name && t.description) tagDescriptions.set(t.name, t.description);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
64
|
+
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
65
|
+
|
|
66
|
+
for (const method of HTTP_METHODS) {
|
|
67
|
+
const operation = (pathItem as Record<string, any>)[method];
|
|
68
|
+
if (!operation || typeof operation !== 'object') continue;
|
|
69
|
+
|
|
70
|
+
const tags: string[] = Array.isArray(operation.tags) && operation.tags.length > 0
|
|
71
|
+
? [operation.tags[0]]
|
|
72
|
+
: ['Default'];
|
|
73
|
+
|
|
74
|
+
for (const tag of tags) {
|
|
75
|
+
if (!tagMap.has(tag)) {
|
|
76
|
+
tagMap.set(tag, { tag, description: tagDescriptions.get(tag), operations: [] });
|
|
77
|
+
}
|
|
78
|
+
tagMap.get(tag)!.operations.push({ method: method.toUpperCase(), path: pathStr, operation });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sort: "Default" last, others alphabetical
|
|
84
|
+
const tagGroups = [...tagMap.values()].sort((a, b) => {
|
|
85
|
+
if (a.tag === 'Default') return 1;
|
|
86
|
+
if (b.tag === 'Default') return -1;
|
|
87
|
+
return a.tag.localeCompare(b.tag);
|
|
88
|
+
});
|
|
89
|
+
|
|
9
90
|
return (
|
|
10
|
-
<div className="clearify-openapi-page" style={{
|
|
11
|
-
<
|
|
91
|
+
<div className="clearify-openapi-page" style={{ maxWidth: 'none' }}>
|
|
92
|
+
<ApiHeader spec={spec} />
|
|
93
|
+
{tagGroups.map((group) => (
|
|
94
|
+
<TagGroup
|
|
95
|
+
key={group.tag}
|
|
96
|
+
tag={group.tag}
|
|
97
|
+
description={group.description}
|
|
98
|
+
operations={group.operations}
|
|
99
|
+
baseUrl={baseUrl}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
12
102
|
</div>
|
|
13
103
|
);
|
|
14
104
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ApiHeaderProps {
|
|
4
|
+
spec: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ApiHeader({ spec }: ApiHeaderProps) {
|
|
8
|
+
const info = spec.info ?? {};
|
|
9
|
+
const servers = spec.servers ?? [];
|
|
10
|
+
const securitySchemes = spec.components?.securitySchemes ?? {};
|
|
11
|
+
const schemeEntries = Object.entries(securitySchemes);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div style={{
|
|
15
|
+
marginBottom: '2rem',
|
|
16
|
+
paddingBottom: '1.5rem',
|
|
17
|
+
borderBottom: '1px solid var(--clearify-border)',
|
|
18
|
+
}}>
|
|
19
|
+
{/* Title + Version */}
|
|
20
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
21
|
+
<h1 style={{
|
|
22
|
+
fontSize: '1.75rem',
|
|
23
|
+
fontWeight: 800,
|
|
24
|
+
color: 'var(--clearify-text)',
|
|
25
|
+
letterSpacing: '-0.035em',
|
|
26
|
+
lineHeight: 1.2,
|
|
27
|
+
margin: 0,
|
|
28
|
+
}}>
|
|
29
|
+
{info.title ?? 'API Reference'}
|
|
30
|
+
</h1>
|
|
31
|
+
{info.version && (
|
|
32
|
+
<span style={{
|
|
33
|
+
fontFamily: 'var(--font-mono)',
|
|
34
|
+
fontSize: '0.75rem',
|
|
35
|
+
fontWeight: 600,
|
|
36
|
+
color: 'var(--clearify-primary)',
|
|
37
|
+
background: 'var(--clearify-primary-soft)',
|
|
38
|
+
padding: '0.15rem 0.5rem',
|
|
39
|
+
borderRadius: '4px',
|
|
40
|
+
}}>
|
|
41
|
+
v{info.version}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
<span style={{
|
|
45
|
+
fontFamily: 'var(--font-mono)',
|
|
46
|
+
fontSize: '0.6875rem',
|
|
47
|
+
color: 'var(--clearify-text-tertiary)',
|
|
48
|
+
}}>
|
|
49
|
+
OAS {spec.openapi ?? '3.0'}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Description */}
|
|
54
|
+
{info.description && (
|
|
55
|
+
<p style={{
|
|
56
|
+
fontSize: '0.9375rem',
|
|
57
|
+
color: 'var(--clearify-text-secondary)',
|
|
58
|
+
marginTop: '0.5rem',
|
|
59
|
+
lineHeight: 1.6,
|
|
60
|
+
maxWidth: '60ch',
|
|
61
|
+
}}>
|
|
62
|
+
{info.description}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{/* Server + Auth row */}
|
|
67
|
+
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', marginTop: '0.875rem' }}>
|
|
68
|
+
{/* Server URL */}
|
|
69
|
+
{servers.length > 0 && (
|
|
70
|
+
<div style={{
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: '0.375rem',
|
|
74
|
+
fontSize: '0.8125rem',
|
|
75
|
+
}}>
|
|
76
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--clearify-text-tertiary)' }}>
|
|
77
|
+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
|
|
78
|
+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
|
|
79
|
+
<line x1="6" y1="6" x2="6.01" y2="6" />
|
|
80
|
+
<line x1="6" y1="18" x2="6.01" y2="18" />
|
|
81
|
+
</svg>
|
|
82
|
+
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--clearify-text-secondary)' }}>
|
|
83
|
+
{servers[0].url}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Auth schemes */}
|
|
89
|
+
{schemeEntries.map(([name, scheme]: [string, any]) => (
|
|
90
|
+
<div
|
|
91
|
+
key={name}
|
|
92
|
+
style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
gap: '0.375rem',
|
|
96
|
+
fontSize: '0.8125rem',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--clearify-text-tertiary)' }}>
|
|
100
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
101
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
102
|
+
</svg>
|
|
103
|
+
<span style={{
|
|
104
|
+
fontFamily: 'var(--font-mono)',
|
|
105
|
+
fontSize: '0.75rem',
|
|
106
|
+
color: 'var(--clearify-text-secondary)',
|
|
107
|
+
background: 'var(--clearify-bg-secondary)',
|
|
108
|
+
padding: '0.1rem 0.4rem',
|
|
109
|
+
borderRadius: '4px',
|
|
110
|
+
border: '1px solid var(--clearify-border)',
|
|
111
|
+
}}>
|
|
112
|
+
{scheme.type === 'http' ? `${scheme.scheme} (${scheme.bearerFormat ?? 'token'})` : `${scheme.type}: ${name}`}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState, useMemo, useRef } from 'react';
|
|
2
|
+
import { generateSnippets } from './generate-snippets.js';
|
|
3
|
+
|
|
4
|
+
interface CodeExamplesProps {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
parameters?: any[];
|
|
9
|
+
requestBody?: Record<string, any>;
|
|
10
|
+
security?: Array<Record<string, string[]>>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CodeExamples({ method, path, baseUrl, parameters, requestBody, security }: CodeExamplesProps) {
|
|
14
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
const codeRef = useRef<HTMLPreElement>(null);
|
|
17
|
+
|
|
18
|
+
const snippets = useMemo(
|
|
19
|
+
() => generateSnippets({ method, path, baseUrl, parameters, requestBody, security }),
|
|
20
|
+
[method, path, baseUrl, parameters, requestBody, security]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const handleCopy = async () => {
|
|
24
|
+
const text = codeRef.current?.textContent ?? snippets[activeTab]?.code ?? '';
|
|
25
|
+
await navigator.clipboard.writeText(text);
|
|
26
|
+
setCopied(true);
|
|
27
|
+
setTimeout(() => setCopied(false), 2000);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{
|
|
32
|
+
border: '1px solid var(--clearify-border)',
|
|
33
|
+
borderRadius: 'var(--clearify-radius)',
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
background: '#1e1e2e',
|
|
36
|
+
}}>
|
|
37
|
+
<div style={{
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
42
|
+
padding: '0 0.5rem',
|
|
43
|
+
}}>
|
|
44
|
+
<div style={{ display: 'flex', gap: '0.125rem' }}>
|
|
45
|
+
{snippets.map((tab, i) => (
|
|
46
|
+
<button
|
|
47
|
+
key={tab.label}
|
|
48
|
+
onClick={() => setActiveTab(i)}
|
|
49
|
+
style={{
|
|
50
|
+
fontFamily: 'var(--font-mono)',
|
|
51
|
+
fontSize: '0.75rem',
|
|
52
|
+
fontWeight: i === activeTab ? 600 : 400,
|
|
53
|
+
padding: '0.5rem 0.625rem',
|
|
54
|
+
background: 'none',
|
|
55
|
+
border: 'none',
|
|
56
|
+
borderBottom: i === activeTab ? '2px solid #818cf8' : '2px solid transparent',
|
|
57
|
+
color: i === activeTab ? '#e2e8f0' : '#94a3b8',
|
|
58
|
+
cursor: 'pointer',
|
|
59
|
+
transition: 'color 0.15s ease',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{tab.label}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
<button
|
|
67
|
+
onClick={handleCopy}
|
|
68
|
+
style={{
|
|
69
|
+
background: 'none',
|
|
70
|
+
border: '1px solid rgba(255,255,255,0.1)',
|
|
71
|
+
borderRadius: '4px',
|
|
72
|
+
padding: '0.2rem 0.5rem',
|
|
73
|
+
cursor: 'pointer',
|
|
74
|
+
fontSize: '0.6875rem',
|
|
75
|
+
color: copied ? '#818cf8' : '#94a3b8',
|
|
76
|
+
fontFamily: 'var(--font-sans)',
|
|
77
|
+
transition: 'all 0.15s ease',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
<pre
|
|
84
|
+
ref={codeRef}
|
|
85
|
+
style={{
|
|
86
|
+
padding: '1rem 1.25rem',
|
|
87
|
+
margin: 0,
|
|
88
|
+
fontFamily: 'var(--font-mono)',
|
|
89
|
+
fontSize: '0.8125rem',
|
|
90
|
+
lineHeight: 1.7,
|
|
91
|
+
color: '#e2e8f0',
|
|
92
|
+
overflow: 'auto',
|
|
93
|
+
maxHeight: '400px',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<code>{snippets[activeTab]?.code ?? ''}</code>
|
|
97
|
+
</pre>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const METHOD_STYLES: Record<string, { bg: string; color: string; darkColor: string }> = {
|
|
4
|
+
GET: { bg: 'rgba(5, 150, 105, 0.12)', color: '#059669', darkColor: '#34d399' },
|
|
5
|
+
POST: { bg: 'rgba(37, 99, 235, 0.12)', color: '#2563eb', darkColor: '#60a5fa' },
|
|
6
|
+
PUT: { bg: 'rgba(217, 119, 6, 0.12)', color: '#d97706', darkColor: '#fbbf24' },
|
|
7
|
+
DELETE: { bg: 'rgba(220, 38, 38, 0.12)', color: '#dc2626', darkColor: '#f87171' },
|
|
8
|
+
PATCH: { bg: 'rgba(124, 58, 237, 0.12)', color: '#7c3aed', darkColor: '#a78bfa' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
interface MethodBadgeProps {
|
|
12
|
+
method: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function MethodBadge({ method }: MethodBadgeProps) {
|
|
16
|
+
const upper = method.toUpperCase();
|
|
17
|
+
const style = METHOD_STYLES[upper] ?? { bg: 'rgba(0,0,0,0.06)', color: 'inherit', darkColor: 'inherit' };
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<span
|
|
22
|
+
className={`clearify-method-badge clearify-method-${upper.toLowerCase()}`}
|
|
23
|
+
style={{
|
|
24
|
+
fontFamily: 'var(--font-mono)',
|
|
25
|
+
fontSize: '0.6875rem',
|
|
26
|
+
fontWeight: 700,
|
|
27
|
+
letterSpacing: '0.04em',
|
|
28
|
+
textTransform: 'uppercase' as const,
|
|
29
|
+
padding: '0.15rem 0.5rem',
|
|
30
|
+
borderRadius: '4px',
|
|
31
|
+
lineHeight: 1.5,
|
|
32
|
+
display: 'inline-block',
|
|
33
|
+
background: style.bg,
|
|
34
|
+
color: style.color,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{upper}
|
|
38
|
+
</span>
|
|
39
|
+
<style>{`
|
|
40
|
+
.dark .clearify-method-get { color: ${METHOD_STYLES.GET.darkColor} !important; }
|
|
41
|
+
.dark .clearify-method-post { color: ${METHOD_STYLES.POST.darkColor} !important; }
|
|
42
|
+
.dark .clearify-method-put { color: ${METHOD_STYLES.PUT.darkColor} !important; }
|
|
43
|
+
.dark .clearify-method-delete { color: ${METHOD_STYLES.DELETE.darkColor} !important; }
|
|
44
|
+
.dark .clearify-method-patch { color: ${METHOD_STYLES.PATCH.darkColor} !important; }
|
|
45
|
+
`}</style>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MethodBadge } from './MethodBadge.js';
|
|
3
|
+
import { ParameterTable } from './ParameterTable.js';
|
|
4
|
+
import { SchemaViewer } from './SchemaViewer.js';
|
|
5
|
+
import { ResponseList } from './ResponseList.js';
|
|
6
|
+
import { CodeExamples } from './CodeExamples.js';
|
|
7
|
+
|
|
8
|
+
interface OperationCardProps {
|
|
9
|
+
method: string;
|
|
10
|
+
path: string;
|
|
11
|
+
operation: Record<string, any>;
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
anchorId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function OperationCard({ method, path, operation, baseUrl, anchorId }: OperationCardProps) {
|
|
17
|
+
const summary = operation.summary ?? operation.description ?? `${method.toUpperCase()} ${path}`;
|
|
18
|
+
const description = operation.description && operation.description !== summary ? operation.description : null;
|
|
19
|
+
const parameters = operation.parameters ?? [];
|
|
20
|
+
const requestBody = operation.requestBody;
|
|
21
|
+
const responses = operation.responses ?? {};
|
|
22
|
+
const security = operation.security;
|
|
23
|
+
const deprecated = !!operation.deprecated;
|
|
24
|
+
|
|
25
|
+
const bodySchema = requestBody?.content?.['application/json']?.schema;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
id={anchorId}
|
|
30
|
+
style={{
|
|
31
|
+
border: '1px solid var(--clearify-border)',
|
|
32
|
+
borderRadius: 'var(--clearify-radius)',
|
|
33
|
+
overflow: 'hidden',
|
|
34
|
+
marginBottom: '1rem',
|
|
35
|
+
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
|
|
36
|
+
}}
|
|
37
|
+
className="clearify-operation-card"
|
|
38
|
+
>
|
|
39
|
+
{/* Two-column layout */}
|
|
40
|
+
<div className="clearify-operation-layout" style={{
|
|
41
|
+
display: 'grid',
|
|
42
|
+
gridTemplateColumns: '1fr 1fr',
|
|
43
|
+
gap: 0,
|
|
44
|
+
}}>
|
|
45
|
+
{/* Left column — documentation */}
|
|
46
|
+
<div style={{
|
|
47
|
+
padding: '1.25rem 1.5rem',
|
|
48
|
+
borderRight: '1px solid var(--clearify-border)',
|
|
49
|
+
}}>
|
|
50
|
+
{/* Method + Path header */}
|
|
51
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', flexWrap: 'wrap' }}>
|
|
52
|
+
<MethodBadge method={method} />
|
|
53
|
+
<span style={{
|
|
54
|
+
fontFamily: 'var(--font-mono)',
|
|
55
|
+
fontSize: '0.875rem',
|
|
56
|
+
fontWeight: 500,
|
|
57
|
+
color: deprecated ? 'var(--clearify-text-tertiary)' : 'var(--clearify-text)',
|
|
58
|
+
textDecoration: deprecated ? 'line-through' : undefined,
|
|
59
|
+
letterSpacing: '-0.01em',
|
|
60
|
+
}}>
|
|
61
|
+
{path}
|
|
62
|
+
</span>
|
|
63
|
+
{deprecated && (
|
|
64
|
+
<span style={{
|
|
65
|
+
fontSize: '0.6875rem',
|
|
66
|
+
fontWeight: 600,
|
|
67
|
+
textTransform: 'uppercase' as const,
|
|
68
|
+
color: '#d97706',
|
|
69
|
+
background: 'rgba(217, 119, 6, 0.1)',
|
|
70
|
+
padding: '0.1rem 0.4rem',
|
|
71
|
+
borderRadius: '4px',
|
|
72
|
+
}}>
|
|
73
|
+
Deprecated
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
{security && security.length > 0 && (
|
|
77
|
+
<span style={{
|
|
78
|
+
fontSize: '0.6875rem',
|
|
79
|
+
color: 'var(--clearify-text-tertiary)',
|
|
80
|
+
display: 'flex',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
gap: '0.25rem',
|
|
83
|
+
}}>
|
|
84
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
85
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
86
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
87
|
+
</svg>
|
|
88
|
+
Auth
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Summary */}
|
|
94
|
+
<div style={{
|
|
95
|
+
fontSize: '0.9375rem',
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
color: 'var(--clearify-text)',
|
|
98
|
+
marginTop: '0.625rem',
|
|
99
|
+
letterSpacing: '-0.01em',
|
|
100
|
+
}}>
|
|
101
|
+
{summary}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Description */}
|
|
105
|
+
{description && (
|
|
106
|
+
<div style={{
|
|
107
|
+
fontSize: '0.8125rem',
|
|
108
|
+
color: 'var(--clearify-text-secondary)',
|
|
109
|
+
marginTop: '0.375rem',
|
|
110
|
+
lineHeight: 1.6,
|
|
111
|
+
}}>
|
|
112
|
+
{description}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Parameters */}
|
|
117
|
+
{parameters.length > 0 && <ParameterTable parameters={parameters} />}
|
|
118
|
+
|
|
119
|
+
{/* Request Body */}
|
|
120
|
+
{bodySchema && (
|
|
121
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
122
|
+
<div style={{
|
|
123
|
+
fontSize: '0.6875rem',
|
|
124
|
+
fontWeight: 600,
|
|
125
|
+
textTransform: 'uppercase' as const,
|
|
126
|
+
letterSpacing: '0.05em',
|
|
127
|
+
color: 'var(--clearify-text-tertiary)',
|
|
128
|
+
marginBottom: '0.375rem',
|
|
129
|
+
}}>
|
|
130
|
+
Request Body
|
|
131
|
+
{requestBody?.required && (
|
|
132
|
+
<span className="clearify-required-badge" style={{
|
|
133
|
+
marginLeft: '0.5rem',
|
|
134
|
+
color: '#d97706',
|
|
135
|
+
}}>
|
|
136
|
+
required
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
<div style={{
|
|
141
|
+
border: '1px solid var(--clearify-border)',
|
|
142
|
+
borderRadius: 'var(--clearify-radius)',
|
|
143
|
+
padding: '0.625rem 0.875rem',
|
|
144
|
+
}}>
|
|
145
|
+
<SchemaViewer schema={bodySchema} />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Responses */}
|
|
151
|
+
<ResponseList responses={responses} />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Right column — code examples */}
|
|
155
|
+
<div style={{
|
|
156
|
+
padding: '1.25rem 1.5rem',
|
|
157
|
+
background: 'var(--clearify-bg-secondary)',
|
|
158
|
+
}}>
|
|
159
|
+
<div style={{
|
|
160
|
+
fontSize: '0.6875rem',
|
|
161
|
+
fontWeight: 600,
|
|
162
|
+
textTransform: 'uppercase' as const,
|
|
163
|
+
letterSpacing: '0.05em',
|
|
164
|
+
color: 'var(--clearify-text-tertiary)',
|
|
165
|
+
marginBottom: '0.625rem',
|
|
166
|
+
}}>
|
|
167
|
+
Examples
|
|
168
|
+
</div>
|
|
169
|
+
<CodeExamples
|
|
170
|
+
method={method}
|
|
171
|
+
path={path}
|
|
172
|
+
baseUrl={baseUrl}
|
|
173
|
+
parameters={parameters}
|
|
174
|
+
requestBody={requestBody}
|
|
175
|
+
security={security}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<style>{`
|
|
181
|
+
.clearify-operation-card:hover {
|
|
182
|
+
border-color: var(--clearify-border-strong) !important;
|
|
183
|
+
box-shadow: var(--clearify-shadow-sm);
|
|
184
|
+
}
|
|
185
|
+
@media (max-width: 900px) {
|
|
186
|
+
.clearify-operation-layout {
|
|
187
|
+
grid-template-columns: 1fr !important;
|
|
188
|
+
}
|
|
189
|
+
.clearify-operation-layout > div:first-child {
|
|
190
|
+
border-right: none !important;
|
|
191
|
+
border-bottom: 1px solid var(--clearify-border) !important;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
.dark .clearify-required-badge { color: #fbbf24 !important; }
|
|
195
|
+
`}</style>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|