@djangocfg/ui-tools 2.1.284 → 2.1.286

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.
Files changed (71) hide show
  1. package/dist/DocsLayout-BCVU6TTX.cjs +2027 -0
  2. package/dist/DocsLayout-BCVU6TTX.cjs.map +1 -0
  3. package/dist/DocsLayout-ERETJLLV.mjs +2020 -0
  4. package/dist/DocsLayout-ERETJLLV.mjs.map +1 -0
  5. package/dist/{PlaygroundLayout-O52C6HK5.css → DocsLayout-MBFIB4NO.css} +1 -1
  6. package/dist/{PrettyCode.client-SGDGQTYT.cjs → PrettyCode.client-5GABIN2I.cjs} +57 -35
  7. package/dist/PrettyCode.client-5GABIN2I.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-DW5LTG47.mjs → PrettyCode.client-IZTXXYHG.mjs} +57 -35
  9. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +1 -0
  10. package/dist/chunk-IULI4XII.cjs +1129 -0
  11. package/dist/chunk-IULI4XII.cjs.map +1 -0
  12. package/dist/chunk-VZGQC3NG.mjs +1100 -0
  13. package/dist/chunk-VZGQC3NG.mjs.map +1 -0
  14. package/dist/index.cjs +88 -552
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +18 -6
  17. package/dist/index.d.ts +18 -6
  18. package/dist/index.mjs +25 -496
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +6 -6
  21. package/src/tools/OpenapiViewer/.claude/.sidecar/activity.jsonl +4 -0
  22. package/src/tools/OpenapiViewer/.claude/.sidecar/map_cache.json +30 -0
  23. package/src/tools/OpenapiViewer/.claude/.sidecar/usage.json +5 -0
  24. package/src/tools/OpenapiViewer/.claude/project-map.md +23 -0
  25. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +28 -2
  26. package/src/tools/OpenapiViewer/README.md +104 -51
  27. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +64 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +137 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +268 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +139 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +211 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +101 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +57 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +11 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +71 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +166 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +121 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts +60 -0
  39. package/src/tools/OpenapiViewer/components/index.ts +5 -2
  40. package/src/tools/OpenapiViewer/components/shared/BodyFormEditor.tsx +422 -0
  41. package/src/tools/OpenapiViewer/components/shared/EndpointDraftSync.tsx +108 -0
  42. package/src/tools/OpenapiViewer/components/shared/EndpointResetButton.tsx +50 -0
  43. package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/RequestPanel.tsx +174 -87
  44. package/src/tools/OpenapiViewer/components/shared/SendButton.tsx +91 -0
  45. package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ui.tsx +5 -4
  46. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +82 -8
  47. package/src/tools/OpenapiViewer/hooks/useEndpointDraft.ts +142 -0
  48. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +126 -13
  49. package/src/tools/OpenapiViewer/index.tsx +3 -7
  50. package/src/tools/OpenapiViewer/lazy.tsx +6 -27
  51. package/src/tools/OpenapiViewer/types.ts +44 -0
  52. package/src/tools/OpenapiViewer/utils/formatters.ts +2 -23
  53. package/src/tools/OpenapiViewer/utils/index.ts +3 -1
  54. package/src/tools/OpenapiViewer/utils/schemaExport.ts +206 -0
  55. package/src/tools/OpenapiViewer/utils/url.ts +202 -0
  56. package/src/tools/PrettyCode/PrettyCode.client.tsx +42 -8
  57. package/src/tools/PrettyCode/index.tsx +6 -0
  58. package/dist/PlaygroundLayout-DHUATCHB.cjs +0 -798
  59. package/dist/PlaygroundLayout-DHUATCHB.cjs.map +0 -1
  60. package/dist/PlaygroundLayout-NONWOVQR.mjs +0 -791
  61. package/dist/PlaygroundLayout-NONWOVQR.mjs.map +0 -1
  62. package/dist/PrettyCode.client-DW5LTG47.mjs.map +0 -1
  63. package/dist/PrettyCode.client-SGDGQTYT.cjs.map +0 -1
  64. package/dist/chunk-5FKE7OME.cjs +0 -369
  65. package/dist/chunk-5FKE7OME.cjs.map +0 -1
  66. package/dist/chunk-BKWDHJKF.mjs +0 -356
  67. package/dist/chunk-BKWDHJKF.mjs.map +0 -1
  68. package/src/tools/OpenapiViewer/components/PlaygroundLayout/EndpointList.tsx +0 -228
  69. package/src/tools/OpenapiViewer/components/PlaygroundLayout/index.tsx +0 -107
  70. /package/dist/{PlaygroundLayout-O52C6HK5.css.map → DocsLayout-MBFIB4NO.css.map} +0 -0
  71. /package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ResponsePanel.tsx +0 -0
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ import type { ApiEndpoint } from '../types';
6
+
7
+ /**
8
+ * Per-endpoint draft kept in localStorage so user input survives across
9
+ * reloads and across switching back-and-forth between endpoints.
10
+ *
11
+ * Scoped by ``schemaId`` + ``method`` + ``path`` — two endpoints on the
12
+ * same path with different methods don't share drafts. Two different
13
+ * APIs don't collide either.
14
+ *
15
+ * Storage shape:
16
+ * { parameters: { [name]: string }, requestBody: string }
17
+ *
18
+ * Implementation note: we deliberately DO NOT build on top of
19
+ * ``useLocalStorage`` here. That hook's ``setValue`` callback reference
20
+ * changes whenever its internal state changes, and when EndpointDraftSync
21
+ * mirrors context → draft, the chain
22
+ * state change ⇒ persist call ⇒ hook state change ⇒ new callback ⇒
23
+ * effect rerun ⇒ persist call …
24
+ * blew up into a maximum-update-depth loop. Writing straight to
25
+ * ``localStorage`` with a stable ref-based writer breaks the cycle.
26
+ */
27
+ export interface EndpointDraft {
28
+ parameters: Record<string, string>;
29
+ requestBody: string;
30
+ }
31
+
32
+ const EMPTY_DRAFT: EndpointDraft = { parameters: {}, requestBody: '' };
33
+
34
+ function storageKey(schemaId: string | null, ep: ApiEndpoint | null): string | null {
35
+ if (!schemaId || !ep) return null;
36
+ return `openapi-playground:draft:${schemaId}:${ep.method}:${ep.path}`;
37
+ }
38
+
39
+ function readDraft(key: string | null): EndpointDraft {
40
+ if (!key || typeof window === 'undefined') return EMPTY_DRAFT;
41
+ try {
42
+ const raw = window.localStorage.getItem(key);
43
+ if (!raw) return EMPTY_DRAFT;
44
+ const parsed = JSON.parse(raw) as Partial<EndpointDraft>;
45
+ return {
46
+ parameters: parsed?.parameters ?? {},
47
+ requestBody: typeof parsed?.requestBody === 'string' ? parsed.requestBody : '',
48
+ };
49
+ } catch {
50
+ return EMPTY_DRAFT;
51
+ }
52
+ }
53
+
54
+ function writeDraft(key: string | null, value: EndpointDraft): void {
55
+ if (!key || typeof window === 'undefined') return;
56
+ try {
57
+ // Skip writes for empty drafts — reduces storage noise and keeps
58
+ // "never edited" endpoints out of storage entirely.
59
+ if (Object.keys(value.parameters).length === 0 && !value.requestBody) {
60
+ window.localStorage.removeItem(key);
61
+ return;
62
+ }
63
+ window.localStorage.setItem(key, JSON.stringify(value));
64
+ } catch {
65
+ // Quota / private mode — silently drop; UI state still works.
66
+ }
67
+ }
68
+
69
+ export interface UseEndpointDraftResult {
70
+ /** Draft snapshot loaded on mount / endpoint change. Does not update
71
+ * as the user types — the caller owns the "live" state (context). */
72
+ draft: EndpointDraft;
73
+ /** Persist the current parameters. Safe to call on every change;
74
+ * writes skip when the endpoint has no key yet. */
75
+ setParameters: (params: Record<string, string>) => void;
76
+ setRequestBody: (body: string) => void;
77
+ /** Wipe the persisted draft for the current endpoint. */
78
+ reset: () => void;
79
+ }
80
+
81
+ export function useEndpointDraft(
82
+ schemaId: string | null,
83
+ endpoint: ApiEndpoint | null,
84
+ ): UseEndpointDraftResult {
85
+ const key = storageKey(schemaId, endpoint);
86
+
87
+ // ``draft`` is reloaded from storage only when the key changes —
88
+ // not when we write. Writes don't need to come back to us because
89
+ // the caller keeps the live state and re-reads on key change.
90
+ const [draft, setDraftSnapshot] = useState<EndpointDraft>(() => readDraft(key));
91
+
92
+ // Track the last loaded key so we reload exactly once per endpoint.
93
+ const loadedKeyRef = useRef<string | null>(key);
94
+ useEffect(() => {
95
+ if (loadedKeyRef.current === key) return;
96
+ loadedKeyRef.current = key;
97
+ setDraftSnapshot(readDraft(key));
98
+ }, [key]);
99
+
100
+ // Keep a ref of the current key so the writers below stay stable
101
+ // across renders — React callback identity was the root cause of
102
+ // the infinite render loop we had before.
103
+ const keyRef = useRef(key);
104
+ useEffect(() => {
105
+ keyRef.current = key;
106
+ }, [key]);
107
+
108
+ // Same for the latest full draft value — both writers need to merge
109
+ // their field into the last known shape before writing.
110
+ const latestRef = useRef<EndpointDraft>(draft);
111
+ useEffect(() => {
112
+ latestRef.current = draft;
113
+ }, [draft]);
114
+
115
+ const setParameters = useCallback((params: Record<string, string>) => {
116
+ const next: EndpointDraft = {
117
+ parameters: params,
118
+ requestBody: latestRef.current.requestBody,
119
+ };
120
+ latestRef.current = next;
121
+ writeDraft(keyRef.current, next);
122
+ }, []);
123
+
124
+ const setRequestBody = useCallback((body: string) => {
125
+ const next: EndpointDraft = {
126
+ parameters: latestRef.current.parameters,
127
+ requestBody: body,
128
+ };
129
+ latestRef.current = next;
130
+ writeDraft(keyRef.current, next);
131
+ }, []);
132
+
133
+ const reset = useCallback(() => {
134
+ latestRef.current = EMPTY_DRAFT;
135
+ if (keyRef.current && typeof window !== 'undefined') {
136
+ try { window.localStorage.removeItem(keyRef.current); } catch { /* noop */ }
137
+ }
138
+ setDraftSnapshot(EMPTY_DRAFT);
139
+ }, []);
140
+
141
+ return { draft, setParameters, setRequestBody, reset };
142
+ }
@@ -3,27 +3,90 @@
3
3
  import consola from 'consola';
4
4
  import { useCallback, useEffect, useMemo, useState } from 'react';
5
5
 
6
- import { ApiEndpoint, OpenApiSchema, SchemaSource, UseOpenApiSchemaReturn } from '../types';
6
+ import { ApiEndpoint, OpenApiInfo, OpenApiSchema, SchemaSource, UseOpenApiSchemaReturn } from '../types';
7
+ import { dereferenceSchema } from '../utils/schemaExport';
8
+ import { joinUrl, resolveBaseUrl } from '../utils/url';
9
+
10
+ // ─── JSON Schema → example value ──────────────────────────────────────────────
11
+
12
+ type JsonSchemaNode = Record<string, unknown> & {
13
+ type?: string;
14
+ properties?: Record<string, JsonSchemaNode>;
15
+ required?: string[];
16
+ items?: JsonSchemaNode;
17
+ enum?: unknown[];
18
+ example?: unknown;
19
+ default?: unknown;
20
+ format?: string;
21
+ };
22
+
23
+ /** Walk a JSON Schema and build a realistic-looking example value. */
24
+ function exampleFromSchema(schema: JsonSchemaNode | undefined, depth = 0): unknown {
25
+ if (!schema || depth > 8) return null;
26
+
27
+ // Respect schema-provided examples first — no need to invent a value
28
+ // when the spec author already did the work.
29
+ if (schema.example !== undefined) return schema.example;
30
+ if (schema.default !== undefined) return schema.default;
31
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
32
+
33
+ switch (schema.type) {
34
+ case 'object': {
35
+ const out: Record<string, unknown> = {};
36
+ const props = schema.properties ?? {};
37
+ for (const [k, v] of Object.entries(props)) {
38
+ out[k] = exampleFromSchema(v, depth + 1);
39
+ }
40
+ return out;
41
+ }
42
+ case 'array':
43
+ return [exampleFromSchema(schema.items, depth + 1)];
44
+ case 'integer':
45
+ case 'number':
46
+ return 0;
47
+ case 'boolean':
48
+ return false;
49
+ case 'string':
50
+ if (schema.format === 'date-time') return new Date().toISOString();
51
+ if (schema.format === 'date') return new Date().toISOString().slice(0, 10);
52
+ if (schema.format === 'email') return 'user@example.com';
53
+ if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com';
54
+ if (schema.format === 'uuid') return '00000000-0000-0000-0000-000000000000';
55
+ return '';
56
+ default:
57
+ // No type (or composed schema like allOf/oneOf we don't unpack) —
58
+ // fall back to an empty object rather than ``null`` so the resulting
59
+ // JSON is still valid-looking.
60
+ if (schema.properties) {
61
+ const out: Record<string, unknown> = {};
62
+ for (const [k, v] of Object.entries(schema.properties)) {
63
+ out[k] = exampleFromSchema(v, depth + 1);
64
+ }
65
+ return out;
66
+ }
67
+ return null;
68
+ }
69
+ }
7
70
 
8
71
  // HTTP methods to extract from OpenAPI schema
9
72
  const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
10
73
 
11
- // Extract endpoints from OpenAPI schema (all methods)
12
- const extractEndpoints = (schema: OpenApiSchema): ApiEndpoint[] => {
74
+ // Extract endpoints from OpenAPI schema (all methods). ``baseUrl`` is
75
+ // resolved by the caller via ``resolveBaseUrl`` — we just paste it onto
76
+ // the front of each path here.
77
+ const extractEndpoints = (schema: OpenApiSchema, baseUrl: string): ApiEndpoint[] => {
13
78
  const endpoints: ApiEndpoint[] = [];
14
79
 
15
80
  if (!schema.paths) return [];
16
81
 
17
- // Get base URL from servers
18
- const baseUrl = schema.servers && schema.servers.length > 0 ? schema.servers[0].url : '';
19
-
20
82
  for (const [path, methods] of Object.entries(schema.paths)) {
21
83
  for (const method of HTTP_METHODS) {
22
84
  const op = (methods as any)[method];
23
85
  if (!op) continue;
24
86
 
25
87
  const methodUpper = method.toUpperCase();
26
- const description = op.description || op.summary || `${methodUpper} ${path}`;
88
+ const summary = (op.summary || '').trim();
89
+ const description = op.description || summary || `${methodUpper} ${path}`;
27
90
  const category = op.tags?.[0] || 'Other';
28
91
 
29
92
  const parameters: Array<{
@@ -59,21 +122,29 @@ const extractEndpoints = (schema: OpenApiSchema): ApiEndpoint[] => {
59
122
  }
60
123
  }
61
124
 
62
- // Extract request body info
63
- let requestBody: { type: string; description?: string } | undefined;
125
+ // Extract request body info — keep the dereferenced schema so
126
+ // downstream UI can render a fields table and generate a starter
127
+ // example instead of showing an opaque ``object`` / ``array`` tag.
128
+ let requestBody: ApiEndpoint['requestBody'];
64
129
  if (op.requestBody) {
65
130
  const content = op.requestBody.content;
66
131
  const mediaType = content?.['application/json'] || content?.[Object.keys(content || {})[0]];
132
+ const rawSchema = mediaType?.schema as JsonSchemaNode | undefined;
67
133
  requestBody = {
68
- type: mediaType?.schema?.type || 'object',
134
+ type: rawSchema?.type || 'object',
69
135
  description: op.requestBody.description,
136
+ schema: rawSchema,
137
+ example: rawSchema
138
+ ? JSON.stringify(exampleFromSchema(rawSchema), null, 2)
139
+ : undefined,
70
140
  };
71
141
  }
72
142
 
73
143
  const endpoint: ApiEndpoint = {
74
144
  name: path.split('/').pop() || path,
75
145
  method: methodUpper,
76
- path: baseUrl + path,
146
+ path: baseUrl ? joinUrl(baseUrl, path) : path,
147
+ summary,
77
148
  description,
78
149
  category,
79
150
  parameters: parameters.length > 0 ? parameters : undefined,
@@ -111,11 +182,15 @@ const fetchSchema = async (url: string): Promise<OpenApiSchema> => {
111
182
  interface UseOpenApiSchemaProps {
112
183
  schemas: SchemaSource[];
113
184
  defaultSchemaId?: string;
185
+ /** Global base URL override from ``PlaygroundConfig.baseUrl``.
186
+ * Per-schema ``SchemaSource.baseUrl`` takes precedence over this. */
187
+ baseUrl?: string;
114
188
  }
115
189
 
116
190
  export default function useOpenApiSchema({
117
191
  schemas,
118
192
  defaultSchemaId,
193
+ baseUrl: configBaseUrl,
119
194
  }: UseOpenApiSchemaProps): UseOpenApiSchemaReturn {
120
195
  const [loading, setLoading] = useState(true);
121
196
  const [error, setError] = useState<string | null>(null);
@@ -136,13 +211,45 @@ export default function useOpenApiSchema({
136
211
  [loadedSchemas, currentSchemaId]
137
212
  );
138
213
 
214
+ // Dereference once per schema load so endpoint extraction sees fully
215
+ // resolved ``$ref`` graphs. The raw schema is still exposed via
216
+ // ``rawSchema`` for Copy-for-AI (juniors may want to hand the raw
217
+ // document to an LLM and have it resolve refs itself).
218
+ const dereferencedSchema = useMemo(
219
+ () => (currentOpenApiSchema ? dereferenceSchema(currentOpenApiSchema) : null),
220
+ [currentOpenApiSchema],
221
+ );
222
+
223
+ // Resolve base URL with priority chain. Centralised in ``resolveBaseUrl``
224
+ // so the same logic is reused by the schema-export utilities.
225
+ const resolvedBaseUrl = useMemo(
226
+ () =>
227
+ resolveBaseUrl({
228
+ schemaSource: currentSchema?.baseUrl,
229
+ config: configBaseUrl,
230
+ fromServers: currentOpenApiSchema?.servers?.[0]?.url,
231
+ }),
232
+ [currentSchema?.baseUrl, configBaseUrl, currentOpenApiSchema],
233
+ );
234
+
139
235
  const endpoints = useMemo(
140
- () => (currentOpenApiSchema ? extractEndpoints(currentOpenApiSchema) : []),
141
- [currentOpenApiSchema]
236
+ () => (dereferencedSchema ? extractEndpoints(dereferencedSchema, resolvedBaseUrl) : []),
237
+ [dereferencedSchema, resolvedBaseUrl]
142
238
  );
143
239
 
144
240
  const categories = useMemo(() => getCategories(endpoints), [endpoints]);
145
241
 
242
+ const schemaInfo = useMemo<OpenApiInfo | null>(() => {
243
+ if (!currentOpenApiSchema?.info) return null;
244
+ const { title, version, description } = currentOpenApiSchema.info;
245
+ return {
246
+ title,
247
+ version,
248
+ description,
249
+ servers: currentOpenApiSchema.servers,
250
+ };
251
+ }, [currentOpenApiSchema]);
252
+
146
253
  // Load schema when current schema changes
147
254
  useEffect(() => {
148
255
  if (!currentSchema) return;
@@ -206,6 +313,12 @@ export default function useOpenApiSchema({
206
313
  categories,
207
314
  schemas,
208
315
  currentSchema,
316
+ schemaInfo,
317
+ rawSchema: currentOpenApiSchema ?? null,
318
+ // Consumers expect ``undefined`` when no base URL was resolved (for
319
+ // conditional ``{ baseUrl?: … }`` plumbing). Turn the empty-string
320
+ // convention from the resolver into undefined at the API boundary.
321
+ resolvedBaseUrl: resolvedBaseUrl || undefined,
209
322
  setCurrentSchema,
210
323
  refresh,
211
324
  };
@@ -4,12 +4,10 @@ import React, { lazy, Suspense } from 'react';
4
4
  import { PlaygroundProvider } from './context/PlaygroundContext';
5
5
  import type { PlaygroundConfig } from './types';
6
6
 
7
- // Lazy load the PlaygroundLayout component
8
- const PlaygroundLayout = lazy(() =>
9
- import('./components/PlaygroundLayout').then((mod) => ({ default: mod.PlaygroundLayout }))
7
+ const DocsLayout = lazy(() =>
8
+ import('./components/DocsLayout').then((mod) => ({ default: mod.DocsLayout }))
10
9
  );
11
10
 
12
- // Loading fallback component
13
11
  const LoadingFallback = () => (
14
12
  <div className="flex items-center justify-center min-h-[400px]">
15
13
  <div className="text-muted-foreground">Loading API Playground...</div>
@@ -24,14 +22,12 @@ export const Playground: React.FC<PlaygroundProps> = ({ config }) => {
24
22
  return (
25
23
  <PlaygroundProvider config={config}>
26
24
  <Suspense fallback={<LoadingFallback />}>
27
- <PlaygroundLayout />
25
+ <DocsLayout />
28
26
  </Suspense>
29
27
  </PlaygroundProvider>
30
28
  );
31
29
  };
32
30
 
33
- // Re-export types for convenience
34
31
  export type { PlaygroundConfig, SchemaSource } from './types';
35
32
 
36
- // Default export for dynamic import
37
33
  export default Playground;
@@ -7,28 +7,20 @@
7
7
  * Use this for automatic code-splitting with Suspense fallback.
8
8
  *
9
9
  * For direct imports without lazy loading, use:
10
- * import OpenapiViewer from '@djangocfg/ui-tools/openapi'
10
+ * import OpenapiViewer from '@djangocfg/ui-tools/openapi'
11
11
  */
12
12
 
13
13
  import * as React from 'react';
14
- import { createLazyComponent, LoadingFallback } from '../../components';
14
+ import { createLazyComponent } from '../../components';
15
15
  import { PlaygroundProvider } from './context/PlaygroundContext';
16
16
  import type { ApiKey, PlaygroundConfig, SchemaSource } from './types';
17
17
 
18
- // ============================================================================
19
- // Types
20
- // ============================================================================
21
-
22
18
  export interface PlaygroundProps {
23
19
  config: PlaygroundConfig;
24
20
  }
25
21
 
26
22
  export type { ApiKey, PlaygroundConfig, SchemaSource };
27
23
 
28
- // ============================================================================
29
- // OpenAPI Loading Fallback
30
- // ============================================================================
31
-
32
24
  function OpenapiLoadingFallback() {
33
25
  return (
34
26
  <div className="flex items-center justify-center min-h-[400px] bg-muted/30 rounded-lg">
@@ -40,31 +32,18 @@ function OpenapiLoadingFallback() {
40
32
  );
41
33
  }
42
34
 
43
- // ============================================================================
44
- // Lazy Component (internal)
45
- // ============================================================================
46
-
47
- const LazyPlaygroundLayout = createLazyComponent(
48
- () => import('./components/PlaygroundLayout').then((mod) => ({ default: mod.PlaygroundLayout })),
35
+ const LazyDocsLayout = createLazyComponent(
36
+ () => import('./components/DocsLayout').then((mod) => ({ default: mod.DocsLayout })),
49
37
  {
50
- displayName: 'LazyPlaygroundLayout',
38
+ displayName: 'LazyDocsLayout',
51
39
  fallback: <OpenapiLoadingFallback />,
52
40
  }
53
41
  );
54
42
 
55
- // ============================================================================
56
- // LazyOpenapiViewer
57
- // ============================================================================
58
-
59
- /**
60
- * LazyOpenapiViewer - Lazy-loaded OpenAPI schema viewer & playground
61
- *
62
- * Automatically shows loading state while OpenAPI components load (~400KB)
63
- */
64
43
  export const LazyOpenapiViewer: React.FC<PlaygroundProps> = ({ config }) => {
65
44
  return (
66
45
  <PlaygroundProvider config={config}>
67
- <LazyPlaygroundLayout />
46
+ <LazyDocsLayout />
68
47
  </PlaygroundProvider>
69
48
  );
70
49
  };
@@ -19,6 +19,9 @@ export interface ApiEndpoint {
19
19
  name: string;
20
20
  method: string;
21
21
  path: string;
22
+ /** Short human label from OpenAPI ``operation.summary``. Empty when
23
+ * the spec provides none. Prefer this for sidebar rows and breadcrumbs. */
24
+ summary: string;
22
25
  description: string;
23
26
  category: string;
24
27
  parameters?: Array<{
@@ -30,6 +33,12 @@ export interface ApiEndpoint {
30
33
  requestBody?: {
31
34
  type: string;
32
35
  description?: string;
36
+ /** Dereferenced JSON Schema for the body. Kept so the docs layout
37
+ * can render a fields table and the playground can seed the body
38
+ * editor with a generated example. */
39
+ schema?: Record<string, unknown>;
40
+ /** Pre-generated example JSON ready to drop into a textarea. */
41
+ example?: string;
33
42
  };
34
43
  responses?: Array<{
35
44
  code: string;
@@ -59,12 +68,23 @@ export interface SchemaSource {
59
68
  id: string;
60
69
  name: string;
61
70
  url: string; // URL to fetch OpenAPI schema from (full URL)
71
+ /** Per-schema override for the request base URL. Wins over the
72
+ * global ``PlaygroundConfig.baseUrl`` and over ``schema.servers[0].url``
73
+ * from the OpenAPI document. Use when a single playground hosts
74
+ * several APIs that live on different domains. */
75
+ baseUrl?: string;
62
76
  }
63
77
 
64
78
  // Playground configuration
65
79
  export interface PlaygroundConfig {
66
80
  schemas: SchemaSource[]; // Array of schema URLs (full URLs from API)
67
81
  defaultSchemaId?: string; // Default schema to select
82
+ /** Global override for the request base URL. Used when the OpenAPI
83
+ * document has no ``servers`` entry, or when docs are hosted on a
84
+ * different origin than the API. Resolution order (highest wins):
85
+ * ``SchemaSource.baseUrl`` → ``PlaygroundConfig.baseUrl`` →
86
+ * ``schema.servers[0].url`` → empty string (relative paths). */
87
+ baseUrl?: string;
68
88
  /** Optional API keys the user can pick from in the request panel.
69
89
  * When provided, the playground auto-selects the first one and
70
90
  * syncs the ``X-API-Key`` header from ``ApiKey.secret``. Pass
@@ -101,6 +121,11 @@ export interface PlaygroundState {
101
121
 
102
122
  // UI state
103
123
  sidebarOpen: boolean;
124
+
125
+ /** Id of the schema the viewer is currently on. Drives per-endpoint
126
+ * draft scoping in localStorage and is pushed in from the layout that
127
+ * owns the ``useOpenApiSchema`` hook. ``null`` before first load. */
128
+ activeSchemaId: string | null;
104
129
  }
105
130
 
106
131
  export type PlaygroundStep = 'endpoints' | 'request' | 'response';
@@ -148,12 +173,23 @@ export interface PlaygroundContextType {
148
173
 
149
174
  // UI management
150
175
  setSidebarOpen: (open: boolean) => void;
176
+ setActiveSchemaId: (id: string | null) => void;
151
177
 
152
178
  // Actions
153
179
  clearAll: () => void;
154
180
  sendRequest: () => Promise<void>;
155
181
  }
156
182
 
183
+ // Subset of OpenAPI ``info`` surfaced to consumers — lets the docs
184
+ // layout render an intro section (title, version, description) without
185
+ // re-parsing the schema. ``servers`` piggy-backs here for the same reason.
186
+ export interface OpenApiInfo {
187
+ title: string;
188
+ version: string;
189
+ description?: string;
190
+ servers?: Array<{ url: string; description?: string }>;
191
+ }
192
+
157
193
  // Hook return types
158
194
  export interface UseOpenApiSchemaReturn {
159
195
  loading: boolean;
@@ -162,6 +198,14 @@ export interface UseOpenApiSchemaReturn {
162
198
  categories: string[];
163
199
  schemas: SchemaSource[];
164
200
  currentSchema: SchemaSource | null;
201
+ /** Parsed ``info`` from the active schema. ``null`` while loading. */
202
+ schemaInfo: OpenApiInfo | null;
203
+ /** Raw parsed OpenAPI document — exposed so consumers can serialise it
204
+ * (Copy-for-AI) without re-fetching. ``null`` while loading. */
205
+ rawSchema: OpenApiSchema | null;
206
+ /** Base URL used for endpoint paths, after applying priority chain
207
+ * (SchemaSource.baseUrl → config.baseUrl → schema.servers[0].url). */
208
+ resolvedBaseUrl: string | undefined;
165
209
  setCurrentSchema: (schemaId: string) => void;
166
210
  refresh: () => void;
167
211
  }
@@ -46,26 +46,5 @@ export const parseRequestHeaders = (headersString: string): Record<string, strin
46
46
  }
47
47
  };
48
48
 
49
- // Substitute URL parameters like {id}, {userId}, etc.
50
- export const substituteUrlParameters = (
51
- url: string,
52
- parameters: Record<string, string>
53
- ): string => {
54
- let substitutedUrl = url;
55
-
56
- Object.entries(parameters).forEach(([key, value]) => {
57
- if (value && value.trim() !== '') {
58
- // Replace both {key} and %7Bkey%7D patterns (URL encoded version)
59
- const patterns = [
60
- new RegExp(`\\{${key}\\}`, 'g'),
61
- new RegExp(`%7B${key}%7D`, 'gi'),
62
- ];
63
-
64
- patterns.forEach((pattern) => {
65
- substitutedUrl = substitutedUrl.replace(pattern, encodeURIComponent(value));
66
- });
67
- }
68
- });
69
-
70
- return substitutedUrl;
71
- };
49
+ // URL helpers moved to ./url.ts — keep this file focused on non-URL
50
+ // formatters (method/status colour maps, JSON validation, header parsing).
@@ -6,4 +6,6 @@
6
6
 
7
7
  export * from './apiKeyManager';
8
8
  export * from './versionManager';
9
- export * from './formatters';
9
+ export * from './formatters';
10
+ export * from './schemaExport';
11
+ export * from './url';