@marlinjai/clearify 1.5.3 → 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/components/OpenAPI.tsx +42 -448
- package/src/theme/components/OpenAPIPage.tsx +93 -3
- 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 +3 -126
|
@@ -1,76 +1,15 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ApiHeader } from './openapi/ApiHeader.js';
|
|
3
|
+
import { TagGroup } from './openapi/TagGroup.js';
|
|
3
4
|
|
|
4
5
|
interface OpenAPIProps {
|
|
5
|
-
|
|
6
|
-
spec?: string | Record<string, unknown>;
|
|
7
|
-
/** URL to fetch the OpenAPI spec from at runtime */
|
|
8
|
-
url?: string;
|
|
9
|
-
/** Hide the built-in Scalar sidebar (default: true, since Clearify has its own sidebar) */
|
|
10
|
-
hideSidebar?: boolean;
|
|
11
|
-
/** Hide the dark mode toggle (default: true, Clearify controls dark mode) */
|
|
12
|
-
hideDarkModeToggle?: boolean;
|
|
13
|
-
/** Hide the search (default: true, Clearify has its own search) */
|
|
14
|
-
hideSearch?: boolean;
|
|
15
|
-
/** Layout style: 'modern' or 'classic' */
|
|
16
|
-
layout?: 'modern' | 'classic';
|
|
17
|
-
/** Enable Scalar's path-based routing. Pass { basePath: '/api' } or true for default '/api'. */
|
|
18
|
-
pathRouting?: { basePath: string } | boolean;
|
|
6
|
+
spec?: Record<string, any>;
|
|
19
7
|
}
|
|
20
8
|
|
|
21
|
-
|
|
22
|
-
const LazyApiReference = typeof window !== 'undefined'
|
|
23
|
-
? React.lazy(() =>
|
|
24
|
-
import('@scalar/api-reference-react').then((mod) => ({
|
|
25
|
-
default: mod.ApiReferenceReact,
|
|
26
|
-
}))
|
|
27
|
-
)
|
|
28
|
-
: null;
|
|
9
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
|
29
10
|
|
|
30
|
-
export function OpenAPI({
|
|
31
|
-
spec
|
|
32
|
-
url,
|
|
33
|
-
hideSidebar = true,
|
|
34
|
-
hideDarkModeToggle = true,
|
|
35
|
-
hideSearch = true,
|
|
36
|
-
layout = 'modern',
|
|
37
|
-
pathRouting,
|
|
38
|
-
}: OpenAPIProps) {
|
|
39
|
-
const { theme } = useTheme();
|
|
40
|
-
|
|
41
|
-
const resolvedSpec = spec ?? null;
|
|
42
|
-
|
|
43
|
-
const configuration = useMemo(() => {
|
|
44
|
-
const config: Record<string, unknown> = {
|
|
45
|
-
theme: 'default',
|
|
46
|
-
forceDarkModeState: theme === 'dark' ? 'dark' : 'light',
|
|
47
|
-
hideDarkModeToggle,
|
|
48
|
-
showSidebar: !hideSidebar,
|
|
49
|
-
hideSearch,
|
|
50
|
-
layout,
|
|
51
|
-
withDefaultFonts: false,
|
|
52
|
-
customCss: SCALAR_CUSTOM_CSS,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
if (pathRouting) {
|
|
56
|
-
config.pathRouting = pathRouting === true
|
|
57
|
-
? { basePath: '/api' }
|
|
58
|
-
: pathRouting;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (url) {
|
|
62
|
-
config.url = url;
|
|
63
|
-
} else if (typeof resolvedSpec === 'string') {
|
|
64
|
-
config.content = resolvedSpec;
|
|
65
|
-
} else if (resolvedSpec && typeof resolvedSpec === 'object') {
|
|
66
|
-
config.content = resolvedSpec;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return config;
|
|
70
|
-
}, [resolvedSpec, url, theme, hideSidebar, hideDarkModeToggle, hideSearch, layout, pathRouting]);
|
|
71
|
-
|
|
72
|
-
// No spec provided via props, config, or URL
|
|
73
|
-
if (!url && !resolvedSpec) {
|
|
11
|
+
export function OpenAPI({ spec }: OpenAPIProps) {
|
|
12
|
+
if (!spec || typeof spec !== 'object') {
|
|
74
13
|
return (
|
|
75
14
|
<div
|
|
76
15
|
style={{
|
|
@@ -81,393 +20,48 @@ export function OpenAPI({
|
|
|
81
20
|
fontSize: '0.875rem',
|
|
82
21
|
}}
|
|
83
22
|
>
|
|
84
|
-
<strong>OpenAPI:</strong> No spec provided. Pass a <code>spec</code> or
|
|
85
|
-
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// SSR fallback
|
|
91
|
-
if (typeof window === 'undefined' || !LazyApiReference) {
|
|
92
|
-
return (
|
|
93
|
-
<div
|
|
94
|
-
style={{
|
|
95
|
-
border: '1px solid var(--clearify-border)',
|
|
96
|
-
borderRadius: 'var(--clearify-radius)',
|
|
97
|
-
padding: '2rem',
|
|
98
|
-
textAlign: 'center',
|
|
99
|
-
color: 'var(--clearify-text-secondary)',
|
|
100
|
-
}}
|
|
101
|
-
>
|
|
102
|
-
Loading API Reference...
|
|
23
|
+
<strong>OpenAPI:</strong> No spec provided. Pass a <code>spec</code> prop or set{' '}
|
|
24
|
+
<code>openapi.spec</code> in your Clearify config.
|
|
103
25
|
</div>
|
|
104
26
|
);
|
|
105
27
|
}
|
|
106
28
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<div
|
|
112
|
-
style={{
|
|
113
|
-
border: '1px solid var(--clearify-border)',
|
|
114
|
-
borderRadius: 'var(--clearify-radius)',
|
|
115
|
-
padding: '2rem',
|
|
116
|
-
textAlign: 'center',
|
|
117
|
-
color: 'var(--clearify-text-secondary)',
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
Loading API Reference...
|
|
121
|
-
</div>
|
|
122
|
-
}
|
|
123
|
-
>
|
|
124
|
-
<LazyApiReference configuration={configuration} />
|
|
125
|
-
</React.Suspense>
|
|
126
|
-
</div>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Custom CSS to integrate Scalar with Clearify's design tokens */
|
|
131
|
-
const SCALAR_CUSTOM_CSS = `
|
|
132
|
-
/* ── Design Token Bridge ── */
|
|
133
|
-
.scalar-app {
|
|
134
|
-
--scalar-font: var(--font-sans);
|
|
135
|
-
--scalar-font-code: var(--font-mono);
|
|
136
|
-
|
|
137
|
-
--scalar-color-1: var(--clearify-text);
|
|
138
|
-
--scalar-color-2: var(--clearify-text-secondary);
|
|
139
|
-
--scalar-color-3: var(--clearify-text-tertiary);
|
|
140
|
-
|
|
141
|
-
--scalar-color-accent: var(--clearify-primary);
|
|
142
|
-
--scalar-background-accent: rgba(99, 102, 241, 0.08);
|
|
143
|
-
|
|
144
|
-
--scalar-background-1: var(--clearify-bg);
|
|
145
|
-
--scalar-background-2: var(--clearify-bg-secondary);
|
|
146
|
-
--scalar-background-3: var(--clearify-bg-tertiary);
|
|
147
|
-
--scalar-background-4: var(--clearify-bg-tertiary);
|
|
148
|
-
|
|
149
|
-
--scalar-border-color: var(--clearify-border-strong);
|
|
150
|
-
--scalar-border-width: 1px;
|
|
151
|
-
|
|
152
|
-
--scalar-radius: var(--clearify-radius-sm);
|
|
153
|
-
--scalar-radius-lg: var(--clearify-radius);
|
|
154
|
-
--scalar-radius-xl: var(--clearify-radius-lg);
|
|
155
|
-
|
|
156
|
-
--scalar-shadow-1: var(--clearify-shadow-sm);
|
|
157
|
-
--scalar-shadow-2: var(--clearify-shadow);
|
|
158
|
-
|
|
159
|
-
--scalar-link-color: var(--clearify-primary);
|
|
160
|
-
--scalar-link-color-hover: var(--clearify-primary-hover);
|
|
161
|
-
--scalar-text-decoration: none;
|
|
162
|
-
--scalar-text-decoration-hover: underline;
|
|
163
|
-
|
|
164
|
-
--scalar-scrollbar-color: var(--clearify-border-strong);
|
|
165
|
-
--scalar-scrollbar-color-active: var(--clearify-text-tertiary);
|
|
166
|
-
|
|
167
|
-
--scalar-color-green: #059669;
|
|
168
|
-
--scalar-color-blue: #2563eb;
|
|
169
|
-
--scalar-color-orange: #d97706;
|
|
170
|
-
--scalar-color-red: #dc2626;
|
|
171
|
-
--scalar-color-purple: #7c3aed;
|
|
172
|
-
--scalar-color-yellow: #ca8a04;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.dark-mode .scalar-app,
|
|
176
|
-
.dark .scalar-app {
|
|
177
|
-
--scalar-background-accent: rgba(129, 140, 248, 0.1);
|
|
178
|
-
--scalar-color-green: #34d399;
|
|
179
|
-
--scalar-color-blue: #60a5fa;
|
|
180
|
-
--scalar-color-orange: #fbbf24;
|
|
181
|
-
--scalar-color-red: #f87171;
|
|
182
|
-
--scalar-color-purple: #a78bfa;
|
|
183
|
-
--scalar-color-yellow: #facc15;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/* ── Layout ── */
|
|
187
|
-
.scalar-app .section {
|
|
188
|
-
padding-left: 0;
|
|
189
|
-
padding-right: 0;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.scalar-app .references-layout {
|
|
193
|
-
--refs-content-max-width: 100%;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* ── Introduction Card ── */
|
|
197
|
-
.scalar-app .introduction-card {
|
|
198
|
-
border: 1px solid var(--clearify-border);
|
|
199
|
-
border-radius: var(--clearify-radius);
|
|
200
|
-
background: var(--clearify-bg-secondary);
|
|
201
|
-
overflow: hidden;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/* ── Section Headers ── */
|
|
205
|
-
.scalar-app .section-header {
|
|
206
|
-
font-family: var(--font-sans);
|
|
207
|
-
letter-spacing: -0.01em;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/* ── Endpoint Cards ── */
|
|
211
|
-
.scalar-app .section-accordion {
|
|
212
|
-
border: 1px solid var(--clearify-border);
|
|
213
|
-
border-radius: var(--clearify-radius);
|
|
214
|
-
overflow: hidden;
|
|
215
|
-
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
.scalar-app .section-accordion:hover {
|
|
219
|
-
border-color: var(--clearify-border-strong);
|
|
220
|
-
box-shadow: var(--clearify-shadow-sm);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.scalar-app .section-accordion-content-card {
|
|
224
|
-
border-radius: var(--clearify-radius);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* ── Method Badges ── */
|
|
228
|
-
.scalar-app .endpoint-method {
|
|
229
|
-
font-family: var(--font-mono);
|
|
230
|
-
font-size: 0.6875rem;
|
|
231
|
-
font-weight: 700;
|
|
232
|
-
letter-spacing: 0.04em;
|
|
233
|
-
text-transform: uppercase;
|
|
234
|
-
padding: 0.15rem 0.5rem;
|
|
235
|
-
border-radius: 4px;
|
|
236
|
-
line-height: 1.5;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.scalar-app .endpoint-method.get {
|
|
240
|
-
background: rgba(5, 150, 105, 0.12);
|
|
241
|
-
color: var(--scalar-color-green);
|
|
242
|
-
}
|
|
243
|
-
.scalar-app .endpoint-method.post {
|
|
244
|
-
background: rgba(37, 99, 235, 0.12);
|
|
245
|
-
color: var(--scalar-color-blue);
|
|
246
|
-
}
|
|
247
|
-
.scalar-app .endpoint-method.put {
|
|
248
|
-
background: rgba(217, 119, 6, 0.12);
|
|
249
|
-
color: var(--scalar-color-orange);
|
|
250
|
-
}
|
|
251
|
-
.scalar-app .endpoint-method.delete {
|
|
252
|
-
background: rgba(220, 38, 38, 0.12);
|
|
253
|
-
color: var(--scalar-color-red);
|
|
254
|
-
}
|
|
255
|
-
.scalar-app .endpoint-method.patch {
|
|
256
|
-
background: rgba(124, 58, 237, 0.12);
|
|
257
|
-
color: var(--scalar-color-purple);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/* ── Endpoint Paths ── */
|
|
261
|
-
.scalar-app .endpoint-path,
|
|
262
|
-
.scalar-app .endpoint-label-path {
|
|
263
|
-
font-family: var(--font-mono);
|
|
264
|
-
font-size: 0.8125rem;
|
|
265
|
-
letter-spacing: -0.01em;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/* ── Request / Response Cards ── */
|
|
269
|
-
.scalar-app .scalar-card {
|
|
270
|
-
border: 1px solid var(--clearify-border);
|
|
271
|
-
border-radius: var(--clearify-radius);
|
|
272
|
-
overflow: hidden;
|
|
273
|
-
}
|
|
29
|
+
const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
|
|
30
|
+
const paths = spec.paths ?? {};
|
|
31
|
+
const tagMap = new Map<string, { tag: string; description?: string; operations: Array<{ method: string; path: string; operation: Record<string, any> }> }>();
|
|
32
|
+
const tagDescriptions = new Map<string, string>();
|
|
274
33
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.scalar-app .scalar-card-header-tabs button {
|
|
281
|
-
font-family: var(--font-mono);
|
|
282
|
-
font-size: 0.75rem;
|
|
283
|
-
font-weight: 500;
|
|
284
|
-
letter-spacing: 0.02em;
|
|
285
|
-
transition: color 0.15s ease, background-color 0.15s ease;
|
|
286
|
-
border-radius: var(--clearify-radius-sm);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/* ── Code Blocks ── */
|
|
290
|
-
.scalar-app .scalar-codeblock-pre,
|
|
291
|
-
.scalar-app .cm-editor {
|
|
292
|
-
font-family: var(--font-mono);
|
|
293
|
-
font-size: 0.8125rem;
|
|
294
|
-
line-height: 1.7;
|
|
295
|
-
border-radius: var(--clearify-radius);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.scalar-app .scalar-code-block {
|
|
299
|
-
border: 1px solid var(--clearify-border);
|
|
300
|
-
border-radius: var(--clearify-radius);
|
|
301
|
-
overflow: hidden;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
.scalar-app .scalar-code-copy {
|
|
305
|
-
border-radius: var(--clearify-radius-sm);
|
|
306
|
-
opacity: 0;
|
|
307
|
-
transition: opacity 0.15s ease;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.scalar-app .scalar-code-block:hover .scalar-code-copy {
|
|
311
|
-
opacity: 1;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* ── Parameters ── */
|
|
315
|
-
.scalar-app .parameter-item {
|
|
316
|
-
border-bottom: 1px solid var(--clearify-border);
|
|
317
|
-
transition: background-color 0.15s ease;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
.scalar-app .parameter-item:last-child {
|
|
321
|
-
border-bottom: none;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.scalar-app .parameter-item:hover {
|
|
325
|
-
background-color: var(--clearify-bg-secondary);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
.scalar-app .parameter-item-name {
|
|
329
|
-
font-family: var(--font-mono);
|
|
330
|
-
font-size: 0.8125rem;
|
|
331
|
-
font-weight: 600;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.scalar-app .parameter-item--required .parameter-item-required-optional {
|
|
335
|
-
color: var(--scalar-color-orange);
|
|
336
|
-
font-size: 0.6875rem;
|
|
337
|
-
font-weight: 600;
|
|
338
|
-
text-transform: uppercase;
|
|
339
|
-
letter-spacing: 0.04em;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/* ── Schema / Properties ── */
|
|
343
|
-
.scalar-app .schema-card {
|
|
344
|
-
border: 1px solid var(--clearify-border);
|
|
345
|
-
border-radius: var(--clearify-radius);
|
|
346
|
-
overflow: hidden;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.scalar-app .property-name {
|
|
350
|
-
font-family: var(--font-mono);
|
|
351
|
-
font-size: 0.8125rem;
|
|
352
|
-
font-weight: 500;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.scalar-app .property-required {
|
|
356
|
-
color: var(--scalar-color-orange);
|
|
357
|
-
font-size: 0.6875rem;
|
|
358
|
-
font-weight: 600;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
.scalar-app .property-read-only {
|
|
362
|
-
color: var(--scalar-color-blue);
|
|
363
|
-
font-size: 0.6875rem;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
.scalar-app .property-write-only {
|
|
367
|
-
color: var(--scalar-color-green);
|
|
368
|
-
font-size: 0.6875rem;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.scalar-app .schema-type {
|
|
372
|
-
font-family: var(--font-mono);
|
|
373
|
-
font-size: 0.75rem;
|
|
374
|
-
color: var(--clearify-text-tertiary);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.scalar-app .property-enum-value {
|
|
378
|
-
font-family: var(--font-mono);
|
|
379
|
-
font-size: 0.75rem;
|
|
380
|
-
background: var(--clearify-bg-secondary);
|
|
381
|
-
border: 1px solid var(--clearify-border);
|
|
382
|
-
border-radius: 4px;
|
|
383
|
-
padding: 0.1rem 0.4rem;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/* ── Client Libraries ── */
|
|
387
|
-
.scalar-app .client-libraries-icon {
|
|
388
|
-
max-width: 48px;
|
|
389
|
-
max-height: 48px;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.scalar-app .client-libraries {
|
|
393
|
-
border: 1px solid var(--clearify-border);
|
|
394
|
-
border-radius: var(--clearify-radius);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/* ── Buttons ── */
|
|
398
|
-
.scalar-app .scalar-button {
|
|
399
|
-
border-radius: var(--clearify-radius-sm);
|
|
400
|
-
font-weight: 500;
|
|
401
|
-
font-size: 0.8125rem;
|
|
402
|
-
transition: all 0.15s ease;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/* ── Tooltips ── */
|
|
406
|
-
.scalar-app .scalar-tooltip {
|
|
407
|
-
font-size: 0.75rem;
|
|
408
|
-
border-radius: var(--clearify-radius-sm);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/* ── Auth Section ── */
|
|
412
|
-
.scalar-app .security-scheme-label {
|
|
413
|
-
font-family: var(--font-mono);
|
|
414
|
-
font-size: 0.75rem;
|
|
415
|
-
border-radius: 4px;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/* ── Smooth Transitions ── */
|
|
419
|
-
.scalar-app .section-accordion-content {
|
|
420
|
-
animation: clearify-fade-in 0.2s ease-out;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.scalar-app .section-accordion-chevron {
|
|
424
|
-
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/* ── Server & Auth Intro Cards ── */
|
|
428
|
-
.scalar-app .introduction-card-item {
|
|
429
|
-
border-bottom: 1px solid var(--clearify-border);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.scalar-app .introduction-card-item:last-child {
|
|
433
|
-
border-bottom: none;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/* ── Operation Details ── */
|
|
437
|
-
.scalar-app .operation-details-card {
|
|
438
|
-
border: 1px solid var(--clearify-border);
|
|
439
|
-
border-radius: var(--clearify-radius);
|
|
440
|
-
overflow: hidden;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* ── Try It / Request Editor ── */
|
|
444
|
-
.scalar-app .request-card {
|
|
445
|
-
border: 1px solid var(--clearify-border);
|
|
446
|
-
border-radius: var(--clearify-radius);
|
|
447
|
-
overflow: hidden;
|
|
34
|
+
if (Array.isArray(spec.tags)) {
|
|
35
|
+
for (const t of spec.tags) {
|
|
36
|
+
if (t.name && t.description) tagDescriptions.set(t.name, t.description);
|
|
37
|
+
}
|
|
448
38
|
}
|
|
449
39
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
40
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
41
|
+
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
42
|
+
for (const method of HTTP_METHODS) {
|
|
43
|
+
const operation = (pathItem as Record<string, any>)[method];
|
|
44
|
+
if (!operation || typeof operation !== 'object') continue;
|
|
45
|
+
const tags: string[] = Array.isArray(operation.tags) && operation.tags.length > 0 ? [operation.tags[0]] : ['Default'];
|
|
46
|
+
for (const tag of tags) {
|
|
47
|
+
if (!tagMap.has(tag)) tagMap.set(tag, { tag, description: tagDescriptions.get(tag), operations: [] });
|
|
48
|
+
tagMap.get(tag)!.operations.push({ method: method.toUpperCase(), path: pathStr, operation });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
454
51
|
}
|
|
455
52
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
--scalar-sidebar-color-active: var(--clearify-primary);
|
|
462
|
-
--scalar-sidebar-border-color: var(--clearify-border);
|
|
463
|
-
--scalar-sidebar-item-hover-background: var(--clearify-bg-secondary);
|
|
464
|
-
--scalar-sidebar-item-active-background: var(--clearify-primary-soft);
|
|
465
|
-
}
|
|
53
|
+
const tagGroups = [...tagMap.values()].sort((a, b) => {
|
|
54
|
+
if (a.tag === 'Default') return 1;
|
|
55
|
+
if (b.tag === 'Default') return -1;
|
|
56
|
+
return a.tag.localeCompare(b.tag);
|
|
57
|
+
});
|
|
466
58
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
59
|
+
return (
|
|
60
|
+
<div className="clearify-openapi-page" style={{ maxWidth: 'none' }}>
|
|
61
|
+
<ApiHeader spec={spec} />
|
|
62
|
+
{tagGroups.map((group) => (
|
|
63
|
+
<TagGroup key={group.tag} tag={group.tag} description={group.description} operations={group.operations} baseUrl={baseUrl} />
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -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
91
|
<div className="clearify-openapi-page" style={{ maxWidth: 'none' }}>
|
|
11
|
-
<
|
|
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
|
}
|