@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.
@@ -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';
@@ -361,3 +361,8 @@ a:hover {
361
361
  0% { background-position: -200% 0; }
362
362
  100% { background-position: 200% 0; }
363
363
  }
364
+
365
+ /* ─── OpenAPI page layout ─── */
366
+ .clearify-openapi-page {
367
+ max-width: none;
368
+ }