@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.
@@ -1,76 +1,15 @@
1
- import React, { useMemo } from 'react';
2
- import { useTheme } from '../ThemeProvider.js';
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
- /** Inline spec as a parsed object or raw string (JSON/YAML). Falls back to config-based spec. */
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
- // SSR guard: Scalar requires DOM
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 <code>url</code> prop,
85
- or set <code>openapi.spec</code> in your Clearify config.
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
- return (
108
- <div className="clearify-openapi-container">
109
- <React.Suspense
110
- fallback={
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
- .scalar-app .scalar-card-header {
276
- background: var(--clearify-bg-secondary);
277
- border-bottom: 1px solid var(--clearify-border);
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
- .scalar-app .response-card {
451
- border: 1px solid var(--clearify-border);
452
- border-radius: var(--clearify-radius);
453
- overflow: hidden;
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
- /* ── Sidebar (when visible) ── */
457
- .scalar-app {
458
- --scalar-sidebar-background-1: var(--clearify-bg);
459
- --scalar-sidebar-color-1: var(--clearify-text);
460
- --scalar-sidebar-color-2: var(--clearify-text-secondary);
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
- /* ── Syntax Highlighting ── */
468
- .scalar-app .hljs-keyword { color: var(--clearify-primary); }
469
- .scalar-app .hljs-string { color: var(--scalar-color-green); }
470
- .scalar-app .hljs-number { color: var(--scalar-color-orange); }
471
- .scalar-app .hljs-attr { color: var(--scalar-color-blue); }
472
- .scalar-app .hljs-comment { color: var(--clearify-text-tertiary); font-style: italic; }
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 basePath = config.openapi?.basePath ?? '/api';
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
- <OpenAPI spec={openapiSpec} hideSidebar={true} pathRouting={{ basePath }} />
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
  }