@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
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
// src/vite-plugin/index.ts
|
|
14
14
|
import { resolve } from "path";
|
|
15
15
|
import { existsSync, readFileSync } from "fs";
|
|
16
|
+
import { dereference } from "@scalar/openapi-parser";
|
|
16
17
|
var VIRTUAL_CONFIG = "virtual:clearify/config";
|
|
17
18
|
var RESOLVED_VIRTUAL_CONFIG = "\0" + VIRTUAL_CONFIG;
|
|
18
19
|
var VIRTUAL_ROUTES = "virtual:clearify/routes";
|
|
@@ -35,7 +36,7 @@ function clearifyPlugin(options = {}) {
|
|
|
35
36
|
let allSectionNavigations;
|
|
36
37
|
let allSearchEntries;
|
|
37
38
|
let mermaidSvgData = options.mermaidSvgs ?? {};
|
|
38
|
-
function refreshDocs() {
|
|
39
|
+
async function refreshDocs() {
|
|
39
40
|
const changelogPath = resolve(userRoot, "CHANGELOG.md");
|
|
40
41
|
sectionDataList = visibleSections.map(
|
|
41
42
|
(section) => buildSectionData(section, changelogPath, config.navigation)
|
|
@@ -48,7 +49,7 @@ function clearifyPlugin(options = {}) {
|
|
|
48
49
|
);
|
|
49
50
|
if (config.openapi?.spec && config.openapi.generatePages !== false) {
|
|
50
51
|
const basePath = config.openapi.basePath ?? "/api";
|
|
51
|
-
const specData = loadOpenAPISpecData();
|
|
52
|
+
const specData = await loadOpenAPISpecData();
|
|
52
53
|
if (specData) {
|
|
53
54
|
let opAnchor2 = function(tag, method, opPath) {
|
|
54
55
|
const slug = `${method.toLowerCase()}${opPath.replace(/[{}]/g, "")}`;
|
|
@@ -132,7 +133,7 @@ ${routeEntries.join(",\n")}
|
|
|
132
133
|
}
|
|
133
134
|
let cachedSpecData = null;
|
|
134
135
|
let specDataInitialized = false;
|
|
135
|
-
function loadOpenAPISpecData() {
|
|
136
|
+
async function loadOpenAPISpecData() {
|
|
136
137
|
if (specDataInitialized) return cachedSpecData;
|
|
137
138
|
specDataInitialized = true;
|
|
138
139
|
const specPath = config.openapi?.spec;
|
|
@@ -148,45 +149,32 @@ ${routeEntries.join(",\n")}
|
|
|
148
149
|
try {
|
|
149
150
|
const content = readFileSync(resolvedPath, "utf-8");
|
|
150
151
|
const isYaml = /\.ya?ml$/i.test(resolvedPath);
|
|
152
|
+
let parsed;
|
|
151
153
|
if (isYaml) {
|
|
152
154
|
try {
|
|
153
|
-
|
|
154
|
-
return cachedSpecData;
|
|
155
|
+
parsed = JSON.parse(content);
|
|
155
156
|
} catch {
|
|
156
157
|
console.warn(" YAML OpenAPI specs require js-yaml for page generation. Install it or use a JSON spec.");
|
|
157
158
|
cachedSpecData = null;
|
|
158
159
|
return null;
|
|
159
160
|
}
|
|
161
|
+
} else {
|
|
162
|
+
parsed = JSON.parse(content);
|
|
160
163
|
}
|
|
161
|
-
|
|
164
|
+
const { schema } = await dereference(parsed);
|
|
165
|
+
cachedSpecData = schema;
|
|
162
166
|
return cachedSpecData;
|
|
163
167
|
} catch {
|
|
164
168
|
cachedSpecData = null;
|
|
165
169
|
return null;
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
|
-
function loadOpenAPISpec() {
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
171
|
-
return "export default null;";
|
|
172
|
-
}
|
|
173
|
-
const resolvedPath = resolve(userRoot, specPath);
|
|
174
|
-
if (!existsSync(resolvedPath)) {
|
|
175
|
-
console.warn(` OpenAPI spec not found: ${resolvedPath}`);
|
|
176
|
-
return "export default null;";
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const content = readFileSync(resolvedPath, "utf-8");
|
|
180
|
-
const isYaml = /\.ya?ml$/i.test(resolvedPath);
|
|
181
|
-
if (isYaml) {
|
|
182
|
-
return `export default ${JSON.stringify(content)};`;
|
|
183
|
-
}
|
|
184
|
-
JSON.parse(content);
|
|
185
|
-
return `export default ${content};`;
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.warn(` Failed to load OpenAPI spec: ${resolvedPath}`, err instanceof Error ? err.message : err);
|
|
172
|
+
async function loadOpenAPISpec() {
|
|
173
|
+
const specData = await loadOpenAPISpecData();
|
|
174
|
+
if (!specData) {
|
|
188
175
|
return "export default null;";
|
|
189
176
|
}
|
|
177
|
+
return `export default ${JSON.stringify(specData)};`;
|
|
190
178
|
}
|
|
191
179
|
const mainPlugin = {
|
|
192
180
|
name: "clearify",
|
|
@@ -199,7 +187,7 @@ ${routeEntries.join(",\n")}
|
|
|
199
187
|
const allSections = resolveSections(config, userRoot);
|
|
200
188
|
const isDev = resolvedViteConfig.command === "serve";
|
|
201
189
|
visibleSections = isDev ? allSections : allSections.filter((s) => !s.draft);
|
|
202
|
-
refreshDocs();
|
|
190
|
+
await refreshDocs();
|
|
203
191
|
},
|
|
204
192
|
resolveId(id) {
|
|
205
193
|
if (id === VIRTUAL_CONFIG) return RESOLVED_VIRTUAL_CONFIG;
|
|
@@ -210,7 +198,7 @@ ${routeEntries.join(",\n")}
|
|
|
210
198
|
if (id === VIRTUAL_OPENAPI_SPEC) return RESOLVED_VIRTUAL_OPENAPI_SPEC;
|
|
211
199
|
return null;
|
|
212
200
|
},
|
|
213
|
-
load(id) {
|
|
201
|
+
async load(id) {
|
|
214
202
|
if (id === RESOLVED_VIRTUAL_CONFIG) {
|
|
215
203
|
return `export default ${JSON.stringify(config)};`;
|
|
216
204
|
}
|
|
@@ -227,7 +215,7 @@ ${routeEntries.join(",\n")}
|
|
|
227
215
|
return `export default ${JSON.stringify(mermaidSvgData)};`;
|
|
228
216
|
}
|
|
229
217
|
if (id === RESOLVED_VIRTUAL_OPENAPI_SPEC) {
|
|
230
|
-
return loadOpenAPISpec();
|
|
218
|
+
return await loadOpenAPISpec();
|
|
231
219
|
}
|
|
232
220
|
return null;
|
|
233
221
|
},
|
|
@@ -248,16 +236,17 @@ ${routeEntries.join(",\n")}
|
|
|
248
236
|
if (changedPath === openapiPath) {
|
|
249
237
|
specDataInitialized = false;
|
|
250
238
|
cachedSpecData = null;
|
|
251
|
-
refreshDocs()
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
239
|
+
refreshDocs().then(() => {
|
|
240
|
+
const specMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_OPENAPI_SPEC);
|
|
241
|
+
if (specMod) server.moduleGraph.invalidateModule(specMod);
|
|
242
|
+
const routesMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES);
|
|
243
|
+
if (routesMod) server.moduleGraph.invalidateModule(routesMod);
|
|
244
|
+
const navMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_NAVIGATION);
|
|
245
|
+
if (navMod) server.moduleGraph.invalidateModule(navMod);
|
|
246
|
+
const searchMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SEARCH_INDEX);
|
|
247
|
+
if (searchMod) server.moduleGraph.invalidateModule(searchMod);
|
|
248
|
+
server.ws.send({ type: "full-reload" });
|
|
249
|
+
});
|
|
261
250
|
}
|
|
262
251
|
});
|
|
263
252
|
}
|
|
@@ -358,14 +347,15 @@ ${routeEntries.join(",\n")}
|
|
|
358
347
|
if (!isChangelog && !isInSection) return;
|
|
359
348
|
if (!path.endsWith(".md") && !path.endsWith(".mdx")) return;
|
|
360
349
|
if (event === "add" || event === "unlink" || event === "change") {
|
|
361
|
-
refreshDocs()
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
350
|
+
refreshDocs().then(() => {
|
|
351
|
+
const routesMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES);
|
|
352
|
+
const navMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_NAVIGATION);
|
|
353
|
+
const searchMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SEARCH_INDEX);
|
|
354
|
+
if (routesMod) server.moduleGraph.invalidateModule(routesMod);
|
|
355
|
+
if (navMod) server.moduleGraph.invalidateModule(navMod);
|
|
356
|
+
if (searchMod) server.moduleGraph.invalidateModule(searchMod);
|
|
357
|
+
server.ws.send({ type: "full-reload" });
|
|
358
|
+
});
|
|
369
359
|
}
|
|
370
360
|
});
|
|
371
361
|
return () => {
|
package/dist/node/node/build.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-7IXPTIHW.js";
|
|
4
4
|
import "../chunk-PRTER35L.js";
|
|
5
5
|
import "../chunk-SCZZB7OE.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-7VYQMIW2.js";
|
|
7
7
|
import "../chunk-B2Q23JW3.js";
|
|
8
8
|
import "../chunk-IBK35HZR.js";
|
|
9
9
|
import "../chunk-V7LLYIRO.js";
|
package/dist/node/node/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-7IXPTIHW.js";
|
|
4
4
|
import {
|
|
5
5
|
remarkMermaidToComponent
|
|
6
6
|
} from "../chunk-PRTER35L.js";
|
|
7
7
|
import "../chunk-SCZZB7OE.js";
|
|
8
8
|
import {
|
|
9
9
|
clearifyPlugin
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-7VYQMIW2.js";
|
|
11
11
|
import "../chunk-B2Q23JW3.js";
|
|
12
12
|
import {
|
|
13
13
|
init
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marlinjai/clearify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@mdx-js/react": "^3.1.1",
|
|
31
31
|
"@mdx-js/rollup": "^3.1.1",
|
|
32
|
-
"@scalar/
|
|
32
|
+
"@scalar/openapi-parser": "^0.10.7",
|
|
33
33
|
"@shikijs/rehype": "^3.22.0",
|
|
34
34
|
"@tailwindcss/typography": "^0.5.19",
|
|
35
35
|
"@tailwindcss/vite": "^4.1.18",
|
package/src/theme/Sidebar.tsx
CHANGED
|
@@ -38,6 +38,14 @@ function findActiveSection(pathname: string, sections: SectionNavigation[]): str
|
|
|
38
38
|
return bestMatch;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function collectGroupKeys(items: NavigationItem[], prefix = ''): string[] {
|
|
42
|
+
return items.flatMap((item) => {
|
|
43
|
+
if (!item.children) return [];
|
|
44
|
+
const key = prefix ? `${prefix}/${item.label}` : item.label;
|
|
45
|
+
return [key, ...collectGroupKeys(item.children, key)];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
function SectionSwitcher({
|
|
42
50
|
sections,
|
|
43
51
|
activeId,
|
|
@@ -121,6 +129,32 @@ export function Sidebar({ sections, open, onClose }: SidebarProps) {
|
|
|
121
129
|
const navigation = legacyNavigation ?? activeSection?.navigation ?? [];
|
|
122
130
|
const showSwitcher = isSectionData && sections.length > 1;
|
|
123
131
|
|
|
132
|
+
// Centralized expansion state
|
|
133
|
+
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => new Set(collectGroupKeys(navigation)));
|
|
134
|
+
|
|
135
|
+
// Re-expand all when section changes
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
setExpandedGroups(new Set(collectGroupKeys(navigation)));
|
|
138
|
+
}, [selectedSectionId]);
|
|
139
|
+
|
|
140
|
+
const allGroupKeys = collectGroupKeys(navigation);
|
|
141
|
+
const hasCollapsibleGroups = allGroupKeys.length >= 2;
|
|
142
|
+
|
|
143
|
+
const handleToggleGroup = (key: string) => {
|
|
144
|
+
setExpandedGroups((prev) => {
|
|
145
|
+
const next = new Set(prev);
|
|
146
|
+
if (next.has(key)) {
|
|
147
|
+
next.delete(key);
|
|
148
|
+
} else {
|
|
149
|
+
next.add(key);
|
|
150
|
+
}
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleExpandAll = () => setExpandedGroups(new Set(allGroupKeys));
|
|
156
|
+
const handleCollapseAll = () => setExpandedGroups(new Set());
|
|
157
|
+
|
|
124
158
|
return (
|
|
125
159
|
<>
|
|
126
160
|
{/* Mobile overlay */}
|
|
@@ -163,9 +197,86 @@ export function Sidebar({ sections, open, onClose }: SidebarProps) {
|
|
|
163
197
|
onSelect={setSelectedSectionId}
|
|
164
198
|
/>
|
|
165
199
|
)}
|
|
200
|
+
{hasCollapsibleGroups && (
|
|
201
|
+
<div style={{
|
|
202
|
+
display: 'flex',
|
|
203
|
+
justifyContent: 'flex-end',
|
|
204
|
+
gap: '0.25rem',
|
|
205
|
+
padding: '0.25rem 0.75rem',
|
|
206
|
+
}}>
|
|
207
|
+
<button
|
|
208
|
+
onClick={handleExpandAll}
|
|
209
|
+
title="Expand all groups"
|
|
210
|
+
style={{
|
|
211
|
+
display: 'flex',
|
|
212
|
+
alignItems: 'center',
|
|
213
|
+
justifyContent: 'center',
|
|
214
|
+
width: 24,
|
|
215
|
+
height: 24,
|
|
216
|
+
background: 'none',
|
|
217
|
+
border: 'none',
|
|
218
|
+
cursor: 'pointer',
|
|
219
|
+
color: 'var(--clearify-text-tertiary)',
|
|
220
|
+
borderRadius: 'var(--clearify-radius-sm)',
|
|
221
|
+
transition: 'color 0.15s, background-color 0.15s',
|
|
222
|
+
}}
|
|
223
|
+
onMouseEnter={(e) => {
|
|
224
|
+
e.currentTarget.style.color = 'var(--clearify-text-secondary)';
|
|
225
|
+
e.currentTarget.style.backgroundColor = 'var(--clearify-bg-secondary)';
|
|
226
|
+
}}
|
|
227
|
+
onMouseLeave={(e) => {
|
|
228
|
+
e.currentTarget.style.color = 'var(--clearify-text-tertiary)';
|
|
229
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
233
|
+
<polyline points="7 8 12 3 17 8" />
|
|
234
|
+
<polyline points="7 16 12 21 17 16" />
|
|
235
|
+
</svg>
|
|
236
|
+
</button>
|
|
237
|
+
<button
|
|
238
|
+
onClick={handleCollapseAll}
|
|
239
|
+
title="Collapse all groups"
|
|
240
|
+
style={{
|
|
241
|
+
display: 'flex',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
justifyContent: 'center',
|
|
244
|
+
width: 24,
|
|
245
|
+
height: 24,
|
|
246
|
+
background: 'none',
|
|
247
|
+
border: 'none',
|
|
248
|
+
cursor: 'pointer',
|
|
249
|
+
color: 'var(--clearify-text-tertiary)',
|
|
250
|
+
borderRadius: 'var(--clearify-radius-sm)',
|
|
251
|
+
transition: 'color 0.15s, background-color 0.15s',
|
|
252
|
+
}}
|
|
253
|
+
onMouseEnter={(e) => {
|
|
254
|
+
e.currentTarget.style.color = 'var(--clearify-text-secondary)';
|
|
255
|
+
e.currentTarget.style.backgroundColor = 'var(--clearify-bg-secondary)';
|
|
256
|
+
}}
|
|
257
|
+
onMouseLeave={(e) => {
|
|
258
|
+
e.currentTarget.style.color = 'var(--clearify-text-tertiary)';
|
|
259
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
263
|
+
<polyline points="7 3 12 8 17 3" />
|
|
264
|
+
<polyline points="7 21 12 16 17 21" />
|
|
265
|
+
</svg>
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
166
269
|
<nav style={{ padding: '0 0.5rem' }}>
|
|
167
270
|
{navigation.map((item, i) => (
|
|
168
|
-
<NavItem
|
|
271
|
+
<NavItem
|
|
272
|
+
key={i}
|
|
273
|
+
item={item}
|
|
274
|
+
depth={0}
|
|
275
|
+
onNavigate={onClose}
|
|
276
|
+
expandedGroups={expandedGroups}
|
|
277
|
+
onToggleGroup={handleToggleGroup}
|
|
278
|
+
groupKeyPrefix=""
|
|
279
|
+
/>
|
|
169
280
|
))}
|
|
170
281
|
</nav>
|
|
171
282
|
</aside>
|
|
@@ -193,16 +304,27 @@ export function Sidebar({ sections, open, onClose }: SidebarProps) {
|
|
|
193
304
|
);
|
|
194
305
|
}
|
|
195
306
|
|
|
196
|
-
|
|
307
|
+
interface NavItemProps {
|
|
308
|
+
item: NavigationItem;
|
|
309
|
+
depth: number;
|
|
310
|
+
onNavigate: () => void;
|
|
311
|
+
expandedGroups: Set<string>;
|
|
312
|
+
onToggleGroup: (key: string) => void;
|
|
313
|
+
groupKeyPrefix: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function NavItem({ item, depth, onNavigate, expandedGroups, onToggleGroup, groupKeyPrefix }: NavItemProps) {
|
|
197
317
|
const location = useLocation();
|
|
198
|
-
const [expanded, setExpanded] = useState(true);
|
|
199
318
|
const isActive = item.path === location.pathname;
|
|
200
319
|
|
|
201
320
|
if (item.children) {
|
|
321
|
+
const groupKey = groupKeyPrefix ? `${groupKeyPrefix}/${item.label}` : item.label;
|
|
322
|
+
const expanded = expandedGroups.has(groupKey);
|
|
323
|
+
|
|
202
324
|
return (
|
|
203
325
|
<div style={{ marginBottom: '0.125rem' }}>
|
|
204
326
|
<button
|
|
205
|
-
onClick={() =>
|
|
327
|
+
onClick={() => onToggleGroup(groupKey)}
|
|
206
328
|
style={{
|
|
207
329
|
display: 'flex',
|
|
208
330
|
alignItems: 'center',
|
|
@@ -246,7 +368,15 @@ function NavItem({ item, depth, onNavigate }: { item: NavigationItem; depth: num
|
|
|
246
368
|
{expanded && (
|
|
247
369
|
<div style={{ animation: 'clearify-fade-in 0.15s ease-out' }}>
|
|
248
370
|
{item.children.map((child, i) => (
|
|
249
|
-
<NavItem
|
|
371
|
+
<NavItem
|
|
372
|
+
key={i}
|
|
373
|
+
item={child}
|
|
374
|
+
depth={depth + 1}
|
|
375
|
+
onNavigate={onNavigate}
|
|
376
|
+
expandedGroups={expandedGroups}
|
|
377
|
+
onToggleGroup={onToggleGroup}
|
|
378
|
+
groupKeyPrefix={groupKey}
|
|
379
|
+
/>
|
|
250
380
|
))}
|
|
251
381
|
</div>
|
|
252
382
|
)}
|
|
@@ -1,77 +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
|
-
// Use 'none' theme so Clearify's CSS variable bridge takes full control
|
|
46
|
-
theme: 'none',
|
|
47
|
-
forceDarkModeState: theme === 'dark' ? 'dark' : 'light',
|
|
48
|
-
hideDarkModeToggle,
|
|
49
|
-
showSidebar: !hideSidebar,
|
|
50
|
-
hideSearch,
|
|
51
|
-
layout,
|
|
52
|
-
withDefaultFonts: false,
|
|
53
|
-
customCss: SCALAR_CUSTOM_CSS,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
if (pathRouting) {
|
|
57
|
-
config.pathRouting = pathRouting === true
|
|
58
|
-
? { basePath: '/api' }
|
|
59
|
-
: pathRouting;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (url) {
|
|
63
|
-
config.url = url;
|
|
64
|
-
} else if (typeof resolvedSpec === 'string') {
|
|
65
|
-
config.content = resolvedSpec;
|
|
66
|
-
} else if (resolvedSpec && typeof resolvedSpec === 'object') {
|
|
67
|
-
config.content = resolvedSpec;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return config;
|
|
71
|
-
}, [resolvedSpec, url, theme, hideSidebar, hideDarkModeToggle, hideSearch, layout, pathRouting]);
|
|
72
|
-
|
|
73
|
-
// No spec provided via props, config, or URL
|
|
74
|
-
if (!url && !resolvedSpec) {
|
|
11
|
+
export function OpenAPI({ spec }: OpenAPIProps) {
|
|
12
|
+
if (!spec || typeof spec !== 'object') {
|
|
75
13
|
return (
|
|
76
14
|
<div
|
|
77
15
|
style={{
|
|
@@ -82,79 +20,48 @@ export function OpenAPI({
|
|
|
82
20
|
fontSize: '0.875rem',
|
|
83
21
|
}}
|
|
84
22
|
>
|
|
85
|
-
<strong>OpenAPI:</strong> No spec provided. Pass a <code>spec</code> or
|
|
86
|
-
|
|
23
|
+
<strong>OpenAPI:</strong> No spec provided. Pass a <code>spec</code> prop or set{' '}
|
|
24
|
+
<code>openapi.spec</code> in your Clearify config.
|
|
87
25
|
</div>
|
|
88
26
|
);
|
|
89
27
|
}
|
|
90
28
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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>();
|
|
33
|
+
|
|
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
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
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
|
+
}
|
|
106
51
|
}
|
|
107
52
|
|
|
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
|
+
});
|
|
58
|
+
|
|
108
59
|
return (
|
|
109
|
-
<div className="clearify-openapi-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
border: '1px solid var(--clearify-border)',
|
|
115
|
-
borderRadius: 'var(--clearify-radius)',
|
|
116
|
-
padding: '2rem',
|
|
117
|
-
textAlign: 'center',
|
|
118
|
-
color: 'var(--clearify-text-secondary)',
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
Loading API Reference...
|
|
122
|
-
</div>
|
|
123
|
-
}
|
|
124
|
-
>
|
|
125
|
-
<LazyApiReference configuration={configuration} />
|
|
126
|
-
</React.Suspense>
|
|
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
|
+
))}
|
|
127
65
|
</div>
|
|
128
66
|
);
|
|
129
67
|
}
|
|
130
|
-
|
|
131
|
-
/** Custom CSS to integrate Scalar with Clearify's design tokens */
|
|
132
|
-
const SCALAR_CUSTOM_CSS = `
|
|
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
|
-
|
|
143
|
-
--scalar-background-1: var(--clearify-bg);
|
|
144
|
-
--scalar-background-2: var(--clearify-bg-secondary);
|
|
145
|
-
--scalar-background-3: var(--clearify-bg-tertiary);
|
|
146
|
-
|
|
147
|
-
--scalar-border-color: var(--clearify-border-strong);
|
|
148
|
-
|
|
149
|
-
--scalar-radius: var(--clearify-radius-sm);
|
|
150
|
-
--scalar-radius-lg: var(--clearify-radius);
|
|
151
|
-
|
|
152
|
-
--scalar-shadow-1: var(--clearify-shadow-sm);
|
|
153
|
-
--scalar-shadow-2: var(--clearify-shadow);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.scalar-app .section {
|
|
157
|
-
padding-left: 0;
|
|
158
|
-
padding-right: 0;
|
|
159
|
-
}
|
|
160
|
-
`;
|