@marlinjai/clearify 1.5.3 → 1.6.1

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,148 @@
1
+ import React, { useState } from 'react';
2
+ import { SchemaViewer } from './SchemaViewer.js';
3
+
4
+ interface ResponseListProps {
5
+ responses: Record<string, any>;
6
+ }
7
+
8
+ function statusColor(code: string): string {
9
+ if (code.startsWith('2')) return '#059669';
10
+ if (code.startsWith('3')) return '#2563eb';
11
+ if (code.startsWith('4')) return '#d97706';
12
+ if (code.startsWith('5')) return '#dc2626';
13
+ return 'var(--clearify-text-secondary)';
14
+ }
15
+
16
+ function statusDarkColor(code: string): string {
17
+ if (code.startsWith('2')) return '#34d399';
18
+ if (code.startsWith('3')) return '#60a5fa';
19
+ if (code.startsWith('4')) return '#fbbf24';
20
+ if (code.startsWith('5')) return '#f87171';
21
+ return 'var(--clearify-text-secondary)';
22
+ }
23
+
24
+ export function ResponseList({ responses }: ResponseListProps) {
25
+ if (!responses || Object.keys(responses).length === 0) return null;
26
+
27
+ const entries = Object.entries(responses).sort(([a], [b]) => a.localeCompare(b));
28
+
29
+ return (
30
+ <div style={{ marginTop: '0.75rem' }}>
31
+ <div style={{
32
+ fontSize: '0.6875rem',
33
+ fontWeight: 600,
34
+ textTransform: 'uppercase' as const,
35
+ letterSpacing: '0.05em',
36
+ color: 'var(--clearify-text-tertiary)',
37
+ marginBottom: '0.375rem',
38
+ }}>
39
+ Responses
40
+ </div>
41
+ <div style={{
42
+ border: '1px solid var(--clearify-border)',
43
+ borderRadius: 'var(--clearify-radius)',
44
+ overflow: 'hidden',
45
+ }}>
46
+ {entries.map(([statusCode, response], i) => (
47
+ <ResponseItem
48
+ key={statusCode}
49
+ statusCode={statusCode}
50
+ response={response}
51
+ isLast={i === entries.length - 1}
52
+ defaultExpanded={i === 0 && statusCode.startsWith('2')}
53
+ />
54
+ ))}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ function ResponseItem({
61
+ statusCode,
62
+ response,
63
+ isLast,
64
+ defaultExpanded,
65
+ }: {
66
+ statusCode: string;
67
+ response: Record<string, any>;
68
+ isLast: boolean;
69
+ defaultExpanded: boolean;
70
+ }) {
71
+ const [expanded, setExpanded] = useState(defaultExpanded);
72
+ const schema = response.content?.['application/json']?.schema;
73
+ const hasDetails = !!(schema || response.description);
74
+
75
+ return (
76
+ <div style={{ borderBottom: isLast ? undefined : '1px solid var(--clearify-border)' }}>
77
+ <button
78
+ onClick={hasDetails ? () => setExpanded(!expanded) : undefined}
79
+ style={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ gap: '0.625rem',
83
+ width: '100%',
84
+ padding: '0.625rem 0.875rem',
85
+ background: 'none',
86
+ border: 'none',
87
+ cursor: hasDetails ? 'pointer' : 'default',
88
+ textAlign: 'left',
89
+ fontFamily: 'var(--font-sans)',
90
+ transition: 'background-color 0.15s ease',
91
+ }}
92
+ className="clearify-response-row"
93
+ >
94
+ {hasDetails && (
95
+ <svg
96
+ width="14"
97
+ height="14"
98
+ viewBox="0 0 24 24"
99
+ fill="none"
100
+ stroke="currentColor"
101
+ strokeWidth="2"
102
+ strokeLinecap="round"
103
+ strokeLinejoin="round"
104
+ style={{
105
+ flexShrink: 0,
106
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
107
+ transition: 'transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
108
+ color: 'var(--clearify-text-tertiary)',
109
+ }}
110
+ >
111
+ <polyline points="9 18 15 12 9 6" />
112
+ </svg>
113
+ )}
114
+ <span
115
+ className={`clearify-status-${statusCode.charAt(0)}xx`}
116
+ style={{
117
+ fontFamily: 'var(--font-mono)',
118
+ fontSize: '0.8125rem',
119
+ fontWeight: 700,
120
+ color: statusColor(statusCode),
121
+ }}
122
+ >
123
+ {statusCode}
124
+ </span>
125
+ <span style={{
126
+ fontSize: '0.8125rem',
127
+ color: 'var(--clearify-text-secondary)',
128
+ }}>
129
+ {response.description || ''}
130
+ </span>
131
+ </button>
132
+
133
+ {expanded && hasDetails && (
134
+ <div style={{ padding: '0 0.875rem 0.75rem 2.5rem' }}>
135
+ {schema && <SchemaViewer schema={schema} />}
136
+ </div>
137
+ )}
138
+
139
+ <style>{`
140
+ .clearify-response-row:hover { background-color: var(--clearify-bg-secondary) !important; }
141
+ .dark .clearify-status-2xx { color: ${statusDarkColor('200')} !important; }
142
+ .dark .clearify-status-3xx { color: ${statusDarkColor('300')} !important; }
143
+ .dark .clearify-status-4xx { color: ${statusDarkColor('400')} !important; }
144
+ .dark .clearify-status-5xx { color: ${statusDarkColor('500')} !important; }
145
+ `}</style>
146
+ </div>
147
+ );
148
+ }
@@ -0,0 +1,377 @@
1
+ import React, { useState } from 'react';
2
+
3
+ interface SchemaViewerProps {
4
+ schema: Record<string, any>;
5
+ name?: string;
6
+ required?: boolean;
7
+ defaultExpanded?: boolean;
8
+ depth?: number;
9
+ }
10
+
11
+ export function SchemaViewer({
12
+ schema,
13
+ name,
14
+ required = false,
15
+ defaultExpanded = true,
16
+ depth = 0,
17
+ }: SchemaViewerProps) {
18
+ if (!schema || typeof schema !== 'object') return null;
19
+
20
+ // Handle oneOf / anyOf
21
+ const variants = schema.oneOf ?? schema.anyOf;
22
+ if (variants && Array.isArray(variants)) {
23
+ return (
24
+ <SchemaVariants
25
+ variants={variants}
26
+ label={schema.oneOf ? 'oneOf' : 'anyOf'}
27
+ name={name}
28
+ required={required}
29
+ depth={depth}
30
+ />
31
+ );
32
+ }
33
+
34
+ // Handle array type
35
+ if (schema.type === 'array' && schema.items) {
36
+ return (
37
+ <SchemaProperty name={name} schema={schema} required={required} depth={depth}>
38
+ <SchemaViewer schema={schema.items} depth={depth + 1} defaultExpanded={depth < 2} />
39
+ </SchemaProperty>
40
+ );
41
+ }
42
+
43
+ // Handle object type (or object with properties)
44
+ if (schema.type === 'object' || schema.properties) {
45
+ const properties = schema.properties ?? {};
46
+ const requiredProps: string[] = schema.required ?? [];
47
+
48
+ if (Object.keys(properties).length === 0 && !name) {
49
+ return (
50
+ <div style={{ fontSize: '0.8125rem', color: 'var(--clearify-text-tertiary)', padding: '0.5rem 0' }}>
51
+ No properties defined
52
+ </div>
53
+ );
54
+ }
55
+
56
+ const propertyEntries = Object.entries(properties);
57
+
58
+ // If this is a named property (nested object), wrap in collapsible
59
+ if (name) {
60
+ return (
61
+ <SchemaProperty name={name} schema={schema} required={required} depth={depth}>
62
+ <div style={{ borderLeft: '2px solid var(--clearify-border)', marginLeft: '0.5rem', paddingLeft: '0.75rem' }}>
63
+ {propertyEntries.map(([propName, propSchema]) => (
64
+ <SchemaViewer
65
+ key={propName}
66
+ schema={propSchema as Record<string, any>}
67
+ name={propName}
68
+ required={requiredProps.includes(propName)}
69
+ depth={depth + 1}
70
+ defaultExpanded={depth < 1}
71
+ />
72
+ ))}
73
+ </div>
74
+ </SchemaProperty>
75
+ );
76
+ }
77
+
78
+ // Top-level object — render properties directly
79
+ return (
80
+ <div>
81
+ {propertyEntries.map(([propName, propSchema]) => (
82
+ <SchemaViewer
83
+ key={propName}
84
+ schema={propSchema as Record<string, any>}
85
+ name={propName}
86
+ required={requiredProps.includes(propName)}
87
+ depth={depth}
88
+ defaultExpanded={depth < 2}
89
+ />
90
+ ))}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ // Primitive type — render as leaf property
96
+ if (name) {
97
+ return <SchemaProperty name={name} schema={schema} required={required} depth={depth} />;
98
+ }
99
+
100
+ // Unnamed primitive (e.g., array items that are strings)
101
+ return (
102
+ <div style={{ fontSize: '0.8125rem', color: 'var(--clearify-text-secondary)', padding: '0.25rem 0' }}>
103
+ <TypeBadge type={schema.type} format={schema.format} />
104
+ {schema.description && (
105
+ <span style={{ marginLeft: '0.5rem' }}>{schema.description}</span>
106
+ )}
107
+ </div>
108
+ );
109
+ }
110
+
111
+ /** A single property row with name, type, required badge, constraints, description */
112
+ function SchemaProperty({
113
+ name,
114
+ schema,
115
+ required,
116
+ depth,
117
+ children,
118
+ }: {
119
+ name?: string;
120
+ schema: Record<string, any>;
121
+ required: boolean;
122
+ depth: number;
123
+ children?: React.ReactNode;
124
+ }) {
125
+ const hasChildren = !!children;
126
+ const [expanded, setExpanded] = useState(depth < 2);
127
+
128
+ return (
129
+ <div
130
+ style={{
131
+ borderBottom: depth === 0 ? '1px solid var(--clearify-border)' : undefined,
132
+ padding: '0.5rem 0',
133
+ }}
134
+ >
135
+ <div
136
+ style={{
137
+ display: 'flex',
138
+ alignItems: 'flex-start',
139
+ gap: '0.5rem',
140
+ cursor: hasChildren ? 'pointer' : undefined,
141
+ }}
142
+ onClick={hasChildren ? () => setExpanded(!expanded) : undefined}
143
+ >
144
+ {hasChildren && (
145
+ <svg
146
+ width="14"
147
+ height="14"
148
+ viewBox="0 0 24 24"
149
+ fill="none"
150
+ stroke="currentColor"
151
+ strokeWidth="2"
152
+ strokeLinecap="round"
153
+ strokeLinejoin="round"
154
+ style={{
155
+ flexShrink: 0,
156
+ marginTop: '0.15rem',
157
+ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
158
+ transition: 'transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
159
+ color: 'var(--clearify-text-tertiary)',
160
+ }}
161
+ >
162
+ <polyline points="9 18 15 12 9 6" />
163
+ </svg>
164
+ )}
165
+ <div style={{ flex: 1, minWidth: 0 }}>
166
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
167
+ {name && (
168
+ <span style={{
169
+ fontFamily: 'var(--font-mono)',
170
+ fontSize: '0.8125rem',
171
+ fontWeight: 600,
172
+ color: 'var(--clearify-text)',
173
+ }}>
174
+ {name}
175
+ </span>
176
+ )}
177
+ <TypeBadge
178
+ type={schema.type === 'array' ? `${schema.items?.type ?? 'any'}[]` : schema.type}
179
+ format={schema.format}
180
+ />
181
+ {required && (
182
+ <span style={{
183
+ fontSize: '0.6875rem',
184
+ fontWeight: 600,
185
+ textTransform: 'uppercase' as const,
186
+ letterSpacing: '0.04em',
187
+ color: '#d97706',
188
+ }}
189
+ className="clearify-required-badge"
190
+ >
191
+ required
192
+ </span>
193
+ )}
194
+ {schema.nullable && (
195
+ <span style={{
196
+ fontSize: '0.6875rem',
197
+ color: 'var(--clearify-text-tertiary)',
198
+ }}>
199
+ nullable
200
+ </span>
201
+ )}
202
+ </div>
203
+
204
+ {schema.description && (
205
+ <div style={{
206
+ fontSize: '0.8125rem',
207
+ color: 'var(--clearify-text-secondary)',
208
+ marginTop: '0.25rem',
209
+ lineHeight: 1.5,
210
+ }}>
211
+ {schema.description}
212
+ </div>
213
+ )}
214
+
215
+ {/* Constraints */}
216
+ <SchemaConstraints schema={schema} />
217
+
218
+ {/* Enum values */}
219
+ {schema.enum && (
220
+ <div style={{ display: 'flex', gap: '0.375rem', flexWrap: 'wrap', marginTop: '0.375rem' }}>
221
+ {schema.enum.map((val: any, i: number) => (
222
+ <span
223
+ key={i}
224
+ style={{
225
+ fontFamily: 'var(--font-mono)',
226
+ fontSize: '0.75rem',
227
+ background: 'var(--clearify-bg-secondary)',
228
+ border: '1px solid var(--clearify-border)',
229
+ borderRadius: '4px',
230
+ padding: '0.1rem 0.4rem',
231
+ }}
232
+ >
233
+ {String(val)}
234
+ </span>
235
+ ))}
236
+ </div>
237
+ )}
238
+
239
+ {/* Example */}
240
+ {schema.example !== undefined && (
241
+ <div style={{ marginTop: '0.25rem', fontSize: '0.75rem', color: 'var(--clearify-text-tertiary)' }}>
242
+ Example: <code style={{ fontFamily: 'var(--font-mono)' }}>{JSON.stringify(schema.example)}</code>
243
+ </div>
244
+ )}
245
+ </div>
246
+ </div>
247
+
248
+ {/* Nested children */}
249
+ {hasChildren && expanded && (
250
+ <div style={{ marginTop: '0.25rem' }}>
251
+ {children}
252
+ </div>
253
+ )}
254
+
255
+ <style>{`
256
+ .dark .clearify-required-badge { color: #fbbf24 !important; }
257
+ `}</style>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ function TypeBadge({ type, format }: { type?: string; format?: string }) {
263
+ if (!type) return null;
264
+ const label = format ? `${type}<${format}>` : type;
265
+ return (
266
+ <span style={{
267
+ fontFamily: 'var(--font-mono)',
268
+ fontSize: '0.75rem',
269
+ color: 'var(--clearify-text-tertiary)',
270
+ background: 'var(--clearify-bg-secondary)',
271
+ padding: '0.05rem 0.375rem',
272
+ borderRadius: '4px',
273
+ border: '1px solid var(--clearify-border)',
274
+ }}>
275
+ {label}
276
+ </span>
277
+ );
278
+ }
279
+
280
+ function SchemaConstraints({ schema }: { schema: Record<string, any> }) {
281
+ const constraints: string[] = [];
282
+ if (schema.minLength !== undefined) constraints.push(`minLength: ${schema.minLength}`);
283
+ if (schema.maxLength !== undefined) constraints.push(`maxLength: ${schema.maxLength}`);
284
+ if (schema.minimum !== undefined) constraints.push(`min: ${schema.minimum}`);
285
+ if (schema.maximum !== undefined) constraints.push(`max: ${schema.maximum}`);
286
+ if (schema.pattern !== undefined) constraints.push(`pattern: ${schema.pattern}`);
287
+ if (schema.minItems !== undefined) constraints.push(`minItems: ${schema.minItems}`);
288
+ if (schema.maxItems !== undefined) constraints.push(`maxItems: ${schema.maxItems}`);
289
+ if (schema.uniqueItems) constraints.push('uniqueItems');
290
+ if (schema.default !== undefined) constraints.push(`default: ${JSON.stringify(schema.default)}`);
291
+
292
+ if (constraints.length === 0) return null;
293
+
294
+ return (
295
+ <div style={{
296
+ display: 'flex',
297
+ gap: '0.375rem',
298
+ flexWrap: 'wrap',
299
+ marginTop: '0.25rem',
300
+ }}>
301
+ {constraints.map((c, i) => (
302
+ <span
303
+ key={i}
304
+ style={{
305
+ fontFamily: 'var(--font-mono)',
306
+ fontSize: '0.6875rem',
307
+ color: 'var(--clearify-text-tertiary)',
308
+ background: 'var(--clearify-bg-secondary)',
309
+ padding: '0.05rem 0.375rem',
310
+ borderRadius: '4px',
311
+ border: '1px solid var(--clearify-border)',
312
+ }}
313
+ >
314
+ {c}
315
+ </span>
316
+ ))}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ function SchemaVariants({
322
+ variants,
323
+ label,
324
+ name,
325
+ required,
326
+ depth,
327
+ }: {
328
+ variants: Record<string, any>[];
329
+ label: string;
330
+ name?: string;
331
+ required: boolean;
332
+ depth: number;
333
+ }) {
334
+ const [activeIndex, setActiveIndex] = useState(0);
335
+
336
+ return (
337
+ <div style={{ padding: '0.5rem 0', borderBottom: depth === 0 ? '1px solid var(--clearify-border)' : undefined }}>
338
+ {name && (
339
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.375rem' }}>
340
+ <span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--clearify-text)' }}>
341
+ {name}
342
+ </span>
343
+ <span style={{ fontSize: '0.6875rem', color: 'var(--clearify-text-tertiary)' }}>{label}</span>
344
+ {required && (
345
+ <span style={{ fontSize: '0.6875rem', fontWeight: 600, textTransform: 'uppercase' as const, letterSpacing: '0.04em', color: '#d97706' }}>
346
+ required
347
+ </span>
348
+ )}
349
+ </div>
350
+ )}
351
+ <div style={{ display: 'flex', gap: '0.25rem', marginBottom: '0.5rem' }}>
352
+ {variants.map((v, i) => (
353
+ <button
354
+ key={i}
355
+ onClick={() => setActiveIndex(i)}
356
+ style={{
357
+ fontFamily: 'var(--font-mono)',
358
+ fontSize: '0.75rem',
359
+ padding: '0.2rem 0.5rem',
360
+ borderRadius: '4px',
361
+ border: '1px solid var(--clearify-border)',
362
+ background: i === activeIndex ? 'var(--clearify-primary-soft)' : 'transparent',
363
+ color: i === activeIndex ? 'var(--clearify-primary)' : 'var(--clearify-text-secondary)',
364
+ cursor: 'pointer',
365
+ fontWeight: i === activeIndex ? 600 : 400,
366
+ }}
367
+ >
368
+ {v.title ?? v.type ?? `Option ${i + 1}`}
369
+ </button>
370
+ ))}
371
+ </div>
372
+ <div style={{ borderLeft: '2px solid var(--clearify-border)', marginLeft: '0.5rem', paddingLeft: '0.75rem' }}>
373
+ <SchemaViewer schema={variants[activeIndex]} depth={depth + 1} />
374
+ </div>
375
+ </div>
376
+ );
377
+ }
@@ -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
+ }