@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { OperationCard } from './OperationCard.js';
|
|
3
|
+
|
|
4
|
+
interface TagGroupProps {
|
|
5
|
+
tag: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
operations: Array<{ method: string; path: string; operation: Record<string, any> }>;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Build an anchor ID matching the sidebar navigation pattern */
|
|
12
|
+
function operationAnchorId(tag: string, method: string, path: string): string {
|
|
13
|
+
const slug = `${method.toLowerCase()}${path.replace(/[{}]/g, '')}`;
|
|
14
|
+
return `tag/${encodeURIComponent(tag)}/${slug}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TagGroup({ tag, description, operations, baseUrl }: TagGroupProps) {
|
|
18
|
+
const [expanded, setExpanded] = useState(true);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
id={`tag/${encodeURIComponent(tag)}`}
|
|
23
|
+
style={{ marginBottom: '2rem' }}
|
|
24
|
+
>
|
|
25
|
+
{/* Tag header */}
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setExpanded(!expanded)}
|
|
28
|
+
style={{
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
gap: '0.625rem',
|
|
32
|
+
width: '100%',
|
|
33
|
+
background: 'none',
|
|
34
|
+
border: 'none',
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
padding: '0.5rem 0',
|
|
37
|
+
textAlign: 'left',
|
|
38
|
+
fontFamily: 'var(--font-sans)',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<svg
|
|
42
|
+
width="18"
|
|
43
|
+
height="18"
|
|
44
|
+
viewBox="0 0 24 24"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="2"
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
style={{
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
53
|
+
transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
54
|
+
color: 'var(--clearify-text-secondary)',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<polyline points="9 18 15 12 9 6" />
|
|
58
|
+
</svg>
|
|
59
|
+
<span style={{
|
|
60
|
+
fontSize: '1.25rem',
|
|
61
|
+
fontWeight: 700,
|
|
62
|
+
color: 'var(--clearify-text)',
|
|
63
|
+
letterSpacing: '-0.02em',
|
|
64
|
+
}}>
|
|
65
|
+
{tag}
|
|
66
|
+
</span>
|
|
67
|
+
<span style={{
|
|
68
|
+
fontSize: '0.75rem',
|
|
69
|
+
color: 'var(--clearify-text-tertiary)',
|
|
70
|
+
fontFamily: 'var(--font-mono)',
|
|
71
|
+
}}>
|
|
72
|
+
{operations.length} endpoint{operations.length !== 1 ? 's' : ''}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
{description && (
|
|
77
|
+
<div style={{
|
|
78
|
+
fontSize: '0.875rem',
|
|
79
|
+
color: 'var(--clearify-text-secondary)',
|
|
80
|
+
marginBottom: '0.75rem',
|
|
81
|
+
paddingLeft: '2.125rem',
|
|
82
|
+
lineHeight: 1.6,
|
|
83
|
+
}}>
|
|
84
|
+
{description}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* Operations */}
|
|
89
|
+
{expanded && (
|
|
90
|
+
<div style={{ animation: 'clearify-fade-in 0.2s ease-out' }}>
|
|
91
|
+
{operations.map(({ method, path, operation }) => (
|
|
92
|
+
<OperationCard
|
|
93
|
+
key={`${method}-${path}`}
|
|
94
|
+
method={method}
|
|
95
|
+
path={path}
|
|
96
|
+
operation={operation}
|
|
97
|
+
baseUrl={baseUrl}
|
|
98
|
+
anchorId={operationAnchorId(tag, method, path)}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
interface SnippetInput {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
parameters?: Array<{ name: string; in: string; required?: boolean; schema?: { type?: string } }>;
|
|
6
|
+
requestBody?: Record<string, any>;
|
|
7
|
+
security?: Array<Record<string, string[]>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function generateSnippets(input: SnippetInput): { label: string; code: string }[] {
|
|
11
|
+
const { method, path, baseUrl, parameters = [], requestBody, security } = input;
|
|
12
|
+
const upper = method.toUpperCase();
|
|
13
|
+
const url = `${baseUrl}${path}`;
|
|
14
|
+
|
|
15
|
+
// Replace path params with placeholder values
|
|
16
|
+
const urlWithParams = url.replace(/\{(\w+)\}/g, ':$1');
|
|
17
|
+
|
|
18
|
+
const hasBody = !!requestBody;
|
|
19
|
+
const bodySchema = requestBody?.content?.['application/json']?.schema;
|
|
20
|
+
const bodyExample = bodySchema ? generateExample(bodySchema) : null;
|
|
21
|
+
const bodyJson = bodyExample ? JSON.stringify(bodyExample, null, 2) : null;
|
|
22
|
+
|
|
23
|
+
const hasAuth = security && security.length > 0;
|
|
24
|
+
const authHeader = hasAuth ? 'Authorization: Bearer YOUR_TOKEN' : '';
|
|
25
|
+
|
|
26
|
+
const queryParams = parameters.filter(p => p.in === 'query');
|
|
27
|
+
const queryString = queryParams.length > 0
|
|
28
|
+
? '?' + queryParams.map(p => `${p.name}=VALUE`).join('&')
|
|
29
|
+
: '';
|
|
30
|
+
|
|
31
|
+
// curl
|
|
32
|
+
const curlParts = [`curl -X ${upper} '${urlWithParams}${queryString}'`];
|
|
33
|
+
if (hasAuth) curlParts.push(` -H '${authHeader}'`);
|
|
34
|
+
if (hasBody) {
|
|
35
|
+
curlParts.push(" -H 'Content-Type: application/json'");
|
|
36
|
+
curlParts.push(` -d '${bodyJson}'`);
|
|
37
|
+
}
|
|
38
|
+
const curl = curlParts.join(' \\\n');
|
|
39
|
+
|
|
40
|
+
// JavaScript fetch
|
|
41
|
+
const fetchHeaders: string[] = [];
|
|
42
|
+
if (hasAuth) fetchHeaders.push(` 'Authorization': 'Bearer YOUR_TOKEN'`);
|
|
43
|
+
if (hasBody) fetchHeaders.push(` 'Content-Type': 'application/json'`);
|
|
44
|
+
|
|
45
|
+
let jsParts = `const response = await fetch('${urlWithParams}${queryString}', {\n method: '${upper}',\n`;
|
|
46
|
+
if (fetchHeaders.length > 0) {
|
|
47
|
+
jsParts += ` headers: {\n${fetchHeaders.join(',\n')}\n },\n`;
|
|
48
|
+
}
|
|
49
|
+
if (hasBody) {
|
|
50
|
+
jsParts += ` body: JSON.stringify(${bodyJson}),\n`;
|
|
51
|
+
}
|
|
52
|
+
jsParts += '});\n\nconst data = await response.json();';
|
|
53
|
+
|
|
54
|
+
// Python requests
|
|
55
|
+
let pyParts = `import requests\n\n`;
|
|
56
|
+
pyParts += `response = requests.${method.toLowerCase()}(\n '${urlWithParams}${queryString}'`;
|
|
57
|
+
if (hasAuth || hasBody) pyParts += ',';
|
|
58
|
+
pyParts += '\n';
|
|
59
|
+
if (hasAuth) {
|
|
60
|
+
pyParts += ` headers={'Authorization': 'Bearer YOUR_TOKEN'}`;
|
|
61
|
+
if (hasBody) pyParts += ',';
|
|
62
|
+
pyParts += '\n';
|
|
63
|
+
}
|
|
64
|
+
if (hasBody) {
|
|
65
|
+
pyParts += ` json=${bodyJson},\n`;
|
|
66
|
+
}
|
|
67
|
+
pyParts += ')\n\ndata = response.json()';
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
{ label: 'curl', code: curl },
|
|
71
|
+
{ label: 'JavaScript', code: jsParts },
|
|
72
|
+
{ label: 'Python', code: pyParts },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Generate a minimal example object from a JSON Schema */
|
|
77
|
+
function generateExample(schema: Record<string, any>): any {
|
|
78
|
+
if (schema.example !== undefined) return schema.example;
|
|
79
|
+
|
|
80
|
+
switch (schema.type) {
|
|
81
|
+
case 'string':
|
|
82
|
+
return schema.enum?.[0] ?? schema.default ?? 'string';
|
|
83
|
+
case 'number':
|
|
84
|
+
case 'integer':
|
|
85
|
+
return schema.default ?? schema.minimum ?? 0;
|
|
86
|
+
case 'boolean':
|
|
87
|
+
return schema.default ?? true;
|
|
88
|
+
case 'array':
|
|
89
|
+
return schema.items ? [generateExample(schema.items)] : [];
|
|
90
|
+
case 'object': {
|
|
91
|
+
const obj: Record<string, any> = {};
|
|
92
|
+
if (schema.properties) {
|
|
93
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
94
|
+
obj[key] = generateExample(prop as Record<string, any>);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ApiHeader } from './ApiHeader.js';
|
|
2
|
+
export { TagGroup } from './TagGroup.js';
|
|
3
|
+
export { OperationCard } from './OperationCard.js';
|
|
4
|
+
export { ParameterTable } from './ParameterTable.js';
|
|
5
|
+
export { SchemaViewer } from './SchemaViewer.js';
|
|
6
|
+
export { CodeExamples } from './CodeExamples.js';
|
|
7
|
+
export { ResponseList } from './ResponseList.js';
|
|
8
|
+
export { MethodBadge } from './MethodBadge.js';
|