@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.
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-SCZZB7OE.js";
9
9
  import {
10
10
  clearifyPlugin
11
- } from "./chunk-UOVBAJMR.js";
11
+ } from "./chunk-7VYQMIW2.js";
12
12
  import {
13
13
  loadUserConfig,
14
14
  resolveConfig,
@@ -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
- cachedSpecData = JSON.parse(content);
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
- cachedSpecData = JSON.parse(content);
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 specPath = config.openapi?.spec;
170
- if (!specPath) {
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
- const specMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_OPENAPI_SPEC);
253
- if (specMod) server.moduleGraph.invalidateModule(specMod);
254
- const routesMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES);
255
- if (routesMod) server.moduleGraph.invalidateModule(routesMod);
256
- const navMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_NAVIGATION);
257
- if (navMod) server.moduleGraph.invalidateModule(navMod);
258
- const searchMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SEARCH_INDEX);
259
- if (searchMod) server.moduleGraph.invalidateModule(searchMod);
260
- server.ws.send({ type: "full-reload" });
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
- const routesMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES);
363
- const navMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_NAVIGATION);
364
- const searchMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SEARCH_INDEX);
365
- if (routesMod) server.moduleGraph.invalidateModule(routesMod);
366
- if (navMod) server.moduleGraph.invalidateModule(navMod);
367
- if (searchMod) server.moduleGraph.invalidateModule(searchMod);
368
- server.ws.send({ type: "full-reload" });
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 () => {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-NY3VVUQ3.js";
3
+ } from "../chunk-7IXPTIHW.js";
4
4
  import "../chunk-PRTER35L.js";
5
5
  import "../chunk-SCZZB7OE.js";
6
- import "../chunk-UOVBAJMR.js";
6
+ import "../chunk-7VYQMIW2.js";
7
7
  import "../chunk-B2Q23JW3.js";
8
8
  import "../chunk-IBK35HZR.js";
9
9
  import "../chunk-V7LLYIRO.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-NY3VVUQ3.js";
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-UOVBAJMR.js";
10
+ } from "../chunk-7VYQMIW2.js";
11
11
  import "../chunk-B2Q23JW3.js";
12
12
  import {
13
13
  init
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  clearifyPlugin
3
- } from "../chunk-UOVBAJMR.js";
3
+ } from "../chunk-7VYQMIW2.js";
4
4
  import "../chunk-B2Q23JW3.js";
5
5
  import "../chunk-IBK35HZR.js";
6
6
  import "../chunk-V7LLYIRO.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/clearify",
3
- "version": "1.5.2",
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/api-reference-react": "^0.8.52",
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",
@@ -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 key={i} item={item} depth={0} onNavigate={onClose} />
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
- function NavItem({ item, depth, onNavigate }: { item: NavigationItem; depth: number; onNavigate: () => void }) {
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={() => setExpanded(!expanded)}
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 key={i} item={child} depth={depth + 1} onNavigate={onNavigate} />
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, { 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
- // 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 <code>url</code> prop,
86
- or set <code>openapi.spec</code> in your Clearify config.
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
- // SSR fallback
92
- if (typeof window === 'undefined' || !LazyApiReference) {
93
- return (
94
- <div
95
- style={{
96
- border: '1px solid var(--clearify-border)',
97
- borderRadius: 'var(--clearify-radius)',
98
- padding: '2rem',
99
- textAlign: 'center',
100
- color: 'var(--clearify-text-secondary)',
101
- }}
102
- >
103
- Loading API Reference...
104
- </div>
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-container">
110
- <React.Suspense
111
- fallback={
112
- <div
113
- style={{
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
- `;