@djangocfg/ui-tools 2.1.287 → 2.1.290
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/README.md +14 -3
- package/dist/DocsLayout-IKH7BLSU.cjs +3464 -0
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/DocsLayout-JPXFUKAR.mjs +3457 -0
- package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
- package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
- package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
- package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
- package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
- package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
- package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
- package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
- package/dist/chunk-EFWOJPA6.mjs.map +1 -0
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -14
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +93 -157
- package/src/tools/OpenapiViewer/README.md +114 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +331 -53
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +40 -11
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
- package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +19 -2
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +38 -21
- package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +168 -50
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +3 -1
- package/src/tools/OpenapiViewer/hooks/useDocsUrlSync.ts +119 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +164 -74
- package/src/tools/OpenapiViewer/types.ts +46 -1
- package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
- package/src/tools/OpenapiViewer/utils/index.ts +3 -0
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
- package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
- package/src/tools/OpenapiViewer/utils/scrollParent.ts +68 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
- package/src/tools/PrettyCode/index.tsx +13 -0
- package/src/tools/PrettyCode/lazy.tsx +5 -0
- package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
- package/dist/DocsLayout-BCVU6TTX.cjs +0 -2027
- package/dist/DocsLayout-BCVU6TTX.cjs.map +0 -1
- package/dist/DocsLayout-ERETJLLV.mjs +0 -2020
- package/dist/DocsLayout-ERETJLLV.mjs.map +0 -1
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
- package/dist/chunk-IULI4XII.cjs.map +0 -1
- package/dist/chunk-VZGQC3NG.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -268
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -211
- package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { keyBy } from 'lodash-es';
|
|
4
5
|
|
|
5
6
|
import { Skeleton, TooltipProvider } from '@djangocfg/ui-core/components';
|
|
6
7
|
import { useMediaQuery } from '@djangocfg/ui-core/hooks';
|
|
7
8
|
|
|
8
9
|
import useOpenApiSchema from '../../hooks/useOpenApiSchema';
|
|
10
|
+
import { useDocsUrlSync, type ParsedHash } from '../../hooks/useDocsUrlSync';
|
|
9
11
|
import { usePlaygroundContext } from '../../context/PlaygroundContext';
|
|
10
12
|
import type { ApiEndpoint } from '../../types';
|
|
11
13
|
import { EndpointDraftSync } from '../shared/EndpointDraftSync';
|
|
14
|
+
import { slugifySchemaId } from './anchor';
|
|
12
15
|
import { DocsSidebar } from './Sidebar';
|
|
13
16
|
import { DocsView, type DocsViewHandle } from './DocsView';
|
|
14
17
|
import { SlideInPlayground } from './SlideInPlayground';
|
|
@@ -24,6 +27,12 @@ export const DocsLayout: React.FC = () => {
|
|
|
24
27
|
// mobile-style ``TryItSheet`` on those viewports.
|
|
25
28
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
|
26
29
|
const isMobile = !isDesktop;
|
|
30
|
+
|
|
31
|
+
const grouping = config.schemaGrouping ?? 'selector';
|
|
32
|
+
const preloadAll = grouping === 'sections';
|
|
33
|
+
const urlSyncEnabled =
|
|
34
|
+
typeof config.urlSync === 'boolean' ? config.urlSync : Boolean(config.urlSync?.enabled);
|
|
35
|
+
|
|
27
36
|
const {
|
|
28
37
|
endpoints,
|
|
29
38
|
schemaInfo,
|
|
@@ -34,13 +43,16 @@ export const DocsLayout: React.FC = () => {
|
|
|
34
43
|
schemas,
|
|
35
44
|
currentSchema,
|
|
36
45
|
setCurrentSchema,
|
|
46
|
+
schemasData,
|
|
37
47
|
} = useOpenApiSchema({
|
|
38
48
|
schemas: config.schemas,
|
|
39
49
|
defaultSchemaId: config.defaultSchemaId,
|
|
40
50
|
baseUrl: config.baseUrl,
|
|
51
|
+
preloadAll,
|
|
41
52
|
});
|
|
42
53
|
|
|
43
54
|
const [activeAnchor, setActiveAnchor] = useState<string | null>(null);
|
|
55
|
+
const [activeSchemaId, setActiveSchemaId] = useState<string | null>(null);
|
|
44
56
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
45
57
|
const docsRef = useRef<DocsViewHandle | null>(null);
|
|
46
58
|
|
|
@@ -49,6 +61,17 @@ export const DocsLayout: React.FC = () => {
|
|
|
49
61
|
// semantic — "which endpoint is loaded into the playground".
|
|
50
62
|
const slideOpen = !isMobile && state.selectedEndpoint !== null;
|
|
51
63
|
|
|
64
|
+
// Per-schema endpoint map for the sections sidebar. ``keyBy`` makes the
|
|
65
|
+
// lookup O(1) at render time instead of scanning schemasData in each
|
|
66
|
+
// CategoryBlock — a win for 10+ schemas.
|
|
67
|
+
const endpointsBySchema = useMemo<Record<string, ApiEndpoint[]>>(() => {
|
|
68
|
+
if (grouping !== 'sections') return {};
|
|
69
|
+
const byId = keyBy(schemasData, (e) => e.source.id);
|
|
70
|
+
const out: Record<string, ApiEndpoint[]> = {};
|
|
71
|
+
for (const src of schemas) out[src.id] = byId[src.id]?.endpoints ?? [];
|
|
72
|
+
return out;
|
|
73
|
+
}, [grouping, schemasData, schemas]);
|
|
74
|
+
|
|
52
75
|
const handleTry = useCallback(
|
|
53
76
|
(ep: ApiEndpoint) => {
|
|
54
77
|
setSelectedEndpoint(ep);
|
|
@@ -61,18 +84,83 @@ export const DocsLayout: React.FC = () => {
|
|
|
61
84
|
setSelectedEndpoint(null);
|
|
62
85
|
}, [setSelectedEndpoint]);
|
|
63
86
|
|
|
64
|
-
const handleNavigate = useCallback(
|
|
65
|
-
|
|
87
|
+
const handleNavigate = useCallback(
|
|
88
|
+
(anchor: string, schemaId?: string | null) => {
|
|
89
|
+
// In selector mode a schema switch may be required before the
|
|
90
|
+
// anchor exists in the DOM — defer the scroll until the next
|
|
91
|
+
// paint so ``useOpenApiSchema`` has a chance to swap endpoints.
|
|
92
|
+
if (schemaId && schemaId !== currentSchema?.id && grouping === 'selector') {
|
|
93
|
+
setCurrentSchema(schemaId);
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
docsRef.current?.scrollToAnchor(anchor);
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
docsRef.current?.scrollToAnchor(anchor);
|
|
100
|
+
},
|
|
101
|
+
[currentSchema?.id, grouping, setCurrentSchema],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const handleActiveChange = useCallback((anchor: string | null, schemaId: string | null) => {
|
|
105
|
+
setActiveAnchor(anchor);
|
|
106
|
+
setActiveSchemaId(schemaId);
|
|
66
107
|
}, []);
|
|
67
|
-
|
|
108
|
+
|
|
109
|
+
// URL sync: read hash on mount / popstate → apply; write hash when
|
|
110
|
+
// scrollspy updates. Only the *effective* active schema goes into the
|
|
111
|
+
// hash — in ``selector`` mode it's the combobox value, in ``sections``
|
|
112
|
+
// mode it's whichever schema the scrollspy is currently inside.
|
|
113
|
+
const effectiveSchemaId = grouping === 'sections' ? activeSchemaId : currentSchema?.id ?? null;
|
|
114
|
+
|
|
115
|
+
const handleHashTarget = useCallback(
|
|
116
|
+
(target: ParsedHash) => {
|
|
117
|
+
if (!target.schemaId && !target.anchor) return;
|
|
118
|
+
|
|
119
|
+
// Schema-id segment may be either the raw id or a slug — match
|
|
120
|
+
// both so copy-pasted URLs survive id changes that don't affect
|
|
121
|
+
// the slug. First match wins.
|
|
122
|
+
const matched = target.schemaId
|
|
123
|
+
? schemas.find((s) => s.id === target.schemaId || slugifySchemaId(s.id) === target.schemaId)
|
|
124
|
+
: null;
|
|
125
|
+
|
|
126
|
+
const needsSchemaSwitch =
|
|
127
|
+
matched && grouping === 'selector' && matched.id !== currentSchema?.id;
|
|
128
|
+
|
|
129
|
+
if (needsSchemaSwitch) {
|
|
130
|
+
setCurrentSchema(matched.id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (target.anchor) {
|
|
134
|
+
const anchor = target.anchor;
|
|
135
|
+
// Wait one frame when a switch happened so the new DOM exists.
|
|
136
|
+
if (needsSchemaSwitch) {
|
|
137
|
+
requestAnimationFrame(() => {
|
|
138
|
+
docsRef.current?.scrollToAnchor(anchor);
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
docsRef.current?.scrollToAnchor(anchor);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[schemas, grouping, currentSchema?.id, setCurrentSchema],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
useDocsUrlSync({
|
|
149
|
+
enabled: urlSyncEnabled,
|
|
150
|
+
currentSchemaId: effectiveSchemaId,
|
|
151
|
+
activeAnchor,
|
|
152
|
+
onHashTarget: handleHashTarget,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── Loading / error branches ─────────────────────────────────────────
|
|
68
156
|
|
|
69
157
|
if (loading) {
|
|
70
158
|
return (
|
|
71
|
-
<div
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
159
|
+
<div className="grid grid-cols-[260px_1fr] items-start">
|
|
160
|
+
<div
|
|
161
|
+
className="sticky top-[var(--navbar-height,64px)] border-r p-3 space-y-1.5 overflow-y-auto"
|
|
162
|
+
style={{ height: 'calc(100dvh - var(--navbar-height, 64px))' }}
|
|
163
|
+
>
|
|
76
164
|
{Array.from({ length: 12 }).map((_, i) => (
|
|
77
165
|
<Skeleton key={i} className="h-8 w-full rounded" />
|
|
78
166
|
))}
|
|
@@ -105,58 +193,88 @@ export const DocsLayout: React.FC = () => {
|
|
|
105
193
|
);
|
|
106
194
|
}
|
|
107
195
|
|
|
108
|
-
// Mobile
|
|
196
|
+
// ─── Mobile: sidebar + docs only, playground opens in sheet ───────────
|
|
197
|
+
|
|
109
198
|
if (isMobile) {
|
|
110
199
|
return (
|
|
111
|
-
<div
|
|
112
|
-
className="flex flex-col overflow-hidden"
|
|
113
|
-
style={{ height: 'calc(100dvh - var(--navbar-height, 64px))' }}
|
|
114
|
-
>
|
|
200
|
+
<div className="flex flex-col">
|
|
115
201
|
<EndpointDraftSync schemaId={currentSchema?.id ?? null} />
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
202
|
+
{grouping === 'sections' ? (
|
|
203
|
+
<DocsView
|
|
204
|
+
ref={docsRef}
|
|
205
|
+
grouping="sections"
|
|
206
|
+
schemasData={schemasData}
|
|
207
|
+
selectedVersion={state.selectedVersion}
|
|
208
|
+
loadedEndpoint={state.selectedEndpoint}
|
|
209
|
+
onTryEndpoint={handleTry}
|
|
210
|
+
onActiveChange={handleActiveChange}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
<DocsView
|
|
214
|
+
ref={docsRef}
|
|
215
|
+
info={schemaInfo}
|
|
216
|
+
rawSchema={rawSchema}
|
|
217
|
+
resolvedBaseUrl={resolvedBaseUrl}
|
|
218
|
+
endpoints={endpoints}
|
|
219
|
+
selectedVersion={state.selectedVersion}
|
|
220
|
+
loadedEndpoint={state.selectedEndpoint}
|
|
221
|
+
onTryEndpoint={handleTry}
|
|
222
|
+
onActiveChange={handleActiveChange}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
127
225
|
<TryItSheet open={sheetOpen} onOpenChange={setSheetOpen} />
|
|
128
226
|
</div>
|
|
129
227
|
);
|
|
130
228
|
}
|
|
131
229
|
|
|
230
|
+
// ─── Desktop ──────────────────────────────────────────────────────────
|
|
231
|
+
|
|
132
232
|
return (
|
|
133
233
|
<TooltipProvider delayDuration={350}>
|
|
134
|
-
<div
|
|
135
|
-
className="grid grid-cols-[260px_minmax(0,1fr)] min-h-0 overflow-hidden"
|
|
136
|
-
style={{ height: 'calc(100dvh - var(--navbar-height, 64px))' }}
|
|
137
|
-
>
|
|
234
|
+
<div className="grid grid-cols-[260px_minmax(0,1fr)] items-start">
|
|
138
235
|
<EndpointDraftSync schemaId={currentSchema?.id ?? null} />
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
236
|
+
<div
|
|
237
|
+
className="sticky top-[var(--navbar-height,64px)]"
|
|
238
|
+
style={{ height: 'calc(100dvh - var(--navbar-height, 64px))' }}
|
|
239
|
+
>
|
|
240
|
+
<DocsSidebar
|
|
241
|
+
info={schemaInfo}
|
|
242
|
+
endpoints={endpoints}
|
|
243
|
+
schemas={schemas}
|
|
244
|
+
currentSchemaId={currentSchema?.id ?? null}
|
|
245
|
+
onSchemaChange={setCurrentSchema}
|
|
246
|
+
activeEndpointId={activeAnchor}
|
|
247
|
+
selectedVersion={state.selectedVersion}
|
|
248
|
+
onNavigate={handleNavigate}
|
|
249
|
+
grouping={grouping}
|
|
250
|
+
endpointsBySchema={endpointsBySchema}
|
|
251
|
+
rawSchema={rawSchema}
|
|
252
|
+
resolvedBaseUrl={resolvedBaseUrl}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
{grouping === 'sections' ? (
|
|
256
|
+
<DocsView
|
|
257
|
+
ref={docsRef}
|
|
258
|
+
grouping="sections"
|
|
259
|
+
schemasData={schemasData}
|
|
260
|
+
selectedVersion={state.selectedVersion}
|
|
261
|
+
loadedEndpoint={state.selectedEndpoint}
|
|
262
|
+
onTryEndpoint={handleTry}
|
|
263
|
+
onActiveChange={handleActiveChange}
|
|
264
|
+
/>
|
|
265
|
+
) : (
|
|
266
|
+
<DocsView
|
|
267
|
+
ref={docsRef}
|
|
268
|
+
info={schemaInfo}
|
|
269
|
+
rawSchema={rawSchema}
|
|
270
|
+
resolvedBaseUrl={resolvedBaseUrl}
|
|
271
|
+
endpoints={endpoints}
|
|
272
|
+
selectedVersion={state.selectedVersion}
|
|
273
|
+
loadedEndpoint={state.selectedEndpoint}
|
|
274
|
+
onTryEndpoint={handleTry}
|
|
275
|
+
onActiveChange={handleActiveChange}
|
|
276
|
+
/>
|
|
277
|
+
)}
|
|
160
278
|
{/* SidePanel renders into <body> via portal, so it floats
|
|
161
279
|
above the whole layout (sidebar + navbar included). */}
|
|
162
280
|
<SlideInPlayground open={slideOpen} onClose={handleCloseSlide} />
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import JsonTree from '../../../../JsonTree';
|
|
4
|
+
import PrettyCode from '../../../../PrettyCode';
|
|
5
|
+
import type { DetectedContent } from './types';
|
|
6
|
+
|
|
7
|
+
// JsonTree config — same shape as the interactive docs example view
|
|
8
|
+
// but with ``showActionButtons: false`` because the copy button in the
|
|
9
|
+
// StatusBar already covers that flow.
|
|
10
|
+
const JSON_TREE_CONFIG = {
|
|
11
|
+
maxAutoExpandDepth: 2,
|
|
12
|
+
maxAutoExpandArrayItems: 10,
|
|
13
|
+
maxAutoExpandObjectKeys: 5,
|
|
14
|
+
maxStringLength: 200,
|
|
15
|
+
collectionLimit: 50,
|
|
16
|
+
showCollectionInfo: true,
|
|
17
|
+
showExpandControls: true,
|
|
18
|
+
showActionButtons: false,
|
|
19
|
+
preserveKeyOrder: true,
|
|
20
|
+
className: 'border-0 rounded-none',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
interface PrettyViewProps {
|
|
24
|
+
/** Parsed JSON tree when the body was JSON. */
|
|
25
|
+
treeData: unknown;
|
|
26
|
+
/** Pre-stringified body — used by the code-highlight branch for
|
|
27
|
+
* non-JSON payloads. */
|
|
28
|
+
rawText: string;
|
|
29
|
+
detected: DetectedContent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** "Pretty" view — rich-rendering branch. JSON renders as ``JsonTree``;
|
|
33
|
+
* anything else goes through ``PrettyCode`` in ``plain`` variant so it
|
|
34
|
+
* flows naturally inside the panel's ``ScrollArea`` — no internal
|
|
35
|
+
* scroll, no floating toolbar fighting the surrounding layout. */
|
|
36
|
+
export function PrettyView({ treeData, rawText, detected }: PrettyViewProps) {
|
|
37
|
+
if (detected.kind === 'json' && treeData != null) {
|
|
38
|
+
return <JsonTree title="Response Body" data={treeData} config={JSON_TREE_CONFIG} />;
|
|
39
|
+
}
|
|
40
|
+
if (!rawText) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="py-10 text-center text-xs text-muted-foreground">
|
|
43
|
+
Empty response body
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
<PrettyCode
|
|
49
|
+
data={rawText}
|
|
50
|
+
language={detected.prism}
|
|
51
|
+
variant="plain"
|
|
52
|
+
isCompact
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Info, ShieldCheck } from 'lucide-react';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
interface PreviewViewProps {
|
|
7
|
+
html: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Heuristic: does ``html`` look like a JS-only single-page app shell?
|
|
11
|
+
*
|
|
12
|
+
* SPAs (Vite, CRA, Next-app dev index) ship a near-empty ``<body>``
|
|
13
|
+
* with a mount div + a ``<script type="module">`` that hydrates the
|
|
14
|
+
* page at runtime. Previewing those in a ``sandbox=""`` iframe is
|
|
15
|
+
* pointless because scripts can't run — the reader sees a blank
|
|
16
|
+
* page and assumes preview is broken.
|
|
17
|
+
*
|
|
18
|
+
* We detect this by extracting body content, stripping scripts, and
|
|
19
|
+
* checking what's left. If it's basically just a mount div, we show
|
|
20
|
+
* an explanatory empty-state instead of an empty iframe. */
|
|
21
|
+
function looksLikeSpaShell(html: string): boolean {
|
|
22
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
23
|
+
const bodyContent = (bodyMatch?.[1] ?? html)
|
|
24
|
+
// Strip all script tags with their contents — runtime-only code
|
|
25
|
+
// doesn't count as visible markup for this check.
|
|
26
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
27
|
+
// Strip comments.
|
|
28
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
if (bodyContent.length === 0) return true;
|
|
32
|
+
|
|
33
|
+
// A single empty container (``<div id="root"></div>`` and friends)
|
|
34
|
+
// is the classic SPA mount point. Anything else — including static
|
|
35
|
+
// server-rendered pages with real content — won't match.
|
|
36
|
+
const singleEmptyContainer = /^<(div|main|section)[^>]*>\s*<\/\1>$/i;
|
|
37
|
+
if (singleEmptyContainer.test(bodyContent)) return true;
|
|
38
|
+
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Render an HTML response inside a sandboxed iframe so the reader can
|
|
43
|
+
* see what the server's error page or template actually looks like.
|
|
44
|
+
*
|
|
45
|
+
* Security model — ``sandbox`` is intentionally **empty**:
|
|
46
|
+
* - no ``allow-scripts`` → JavaScript in the response cannot run
|
|
47
|
+
* - no ``allow-same-origin`` → the page can't read parent cookies
|
|
48
|
+
* - no ``allow-forms`` / ``allow-popups`` → can't phish the user
|
|
49
|
+
*
|
|
50
|
+
* We feed the HTML via ``srcDoc`` (not ``src``) so we never issue a
|
|
51
|
+
* network request to render it — the string is already local. */
|
|
52
|
+
export function PreviewView({ html }: PreviewViewProps) {
|
|
53
|
+
const isSpaShell = useMemo(() => looksLikeSpaShell(html), [html]);
|
|
54
|
+
|
|
55
|
+
if (!html) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="py-10 text-center text-xs text-muted-foreground">
|
|
58
|
+
Empty response body
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isSpaShell) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col items-center justify-center py-16 px-6 text-center gap-3 min-h-[400px]">
|
|
66
|
+
<div className="inline-flex items-center justify-center h-10 w-10 rounded-full bg-muted">
|
|
67
|
+
<Info className="h-5 w-5 text-muted-foreground" />
|
|
68
|
+
</div>
|
|
69
|
+
<div className="max-w-sm space-y-1.5">
|
|
70
|
+
<p className="text-sm font-medium text-foreground">
|
|
71
|
+
Looks like a single-page app shell
|
|
72
|
+
</p>
|
|
73
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
74
|
+
This page renders its content with JavaScript at runtime.
|
|
75
|
+
Scripts are disabled in the sandbox, so Preview would show
|
|
76
|
+
a blank page. Switch to <strong>Pretty</strong> or{' '}
|
|
77
|
+
<strong>Raw</strong> to inspect the HTML source.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex flex-col h-full min-h-[400px]">
|
|
86
|
+
<div className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 bg-muted/30 border-b text-[10px] text-muted-foreground/70">
|
|
87
|
+
<ShieldCheck className="h-3 w-3" />
|
|
88
|
+
Sandboxed preview — scripts, forms and popups are disabled
|
|
89
|
+
</div>
|
|
90
|
+
{/*
|
|
91
|
+
* Checker-pattern background so "black on black" responses
|
|
92
|
+
* still show a clear iframe bounding box, and so readers
|
|
93
|
+
* can tell the iframe has loaded even if the page itself
|
|
94
|
+
* is empty / fully transparent.
|
|
95
|
+
*/}
|
|
96
|
+
<div
|
|
97
|
+
className="flex-1 min-h-[360px] p-2"
|
|
98
|
+
style={{
|
|
99
|
+
backgroundColor: '#fff',
|
|
100
|
+
backgroundImage:
|
|
101
|
+
'linear-gradient(45deg, #f3f4f6 25%, transparent 25%), linear-gradient(-45deg, #f3f4f6 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f3f4f6 75%), linear-gradient(-45deg, transparent 75%, #f3f4f6 75%)',
|
|
102
|
+
backgroundSize: '16px 16px',
|
|
103
|
+
backgroundPosition: '0 0, 0 8px, 8px -8px, -8px 0px',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<iframe
|
|
107
|
+
title="Response preview"
|
|
108
|
+
srcDoc={html}
|
|
109
|
+
sandbox=""
|
|
110
|
+
className="w-full h-full min-h-[360px] bg-white border-0 rounded shadow-sm"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface RawViewProps {
|
|
4
|
+
rawText: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Verbatim response body. No parsing, no highlighting — readers
|
|
8
|
+
* occasionally need to inspect trailing whitespace, escape
|
|
9
|
+
* sequences, or a payload that accidentally claims one content type
|
|
10
|
+
* and ships another, and the Pretty view hides some of that. */
|
|
11
|
+
export function RawView({ rawText }: RawViewProps) {
|
|
12
|
+
if (!rawText) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="py-10 text-center text-xs text-muted-foreground">
|
|
15
|
+
Empty response body
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<pre className="p-4 text-[11px] font-mono text-foreground/70 whitespace-pre-wrap break-all leading-relaxed">
|
|
21
|
+
{rawText}
|
|
22
|
+
</pre>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
4
|
+
|
|
5
|
+
import type { ApiResponse } from '../../../types';
|
|
6
|
+
import { StatusBadge } from '../ui';
|
|
7
|
+
|
|
8
|
+
interface StatusBarProps {
|
|
9
|
+
response: ApiResponse;
|
|
10
|
+
rawText: string;
|
|
11
|
+
/** Content-Type label shown inline next to size/duration. ``null``
|
|
12
|
+
* when the server didn't send one — we hide the slot instead of
|
|
13
|
+
* showing an empty dash. */
|
|
14
|
+
contentType: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Top strip of the response panel: status badge, statusText, size,
|
|
18
|
+
* duration, content-type, copy button. Compact — one line, tabular
|
|
19
|
+
* numerals so 1.4 KB and 107ms don't shift as values update. */
|
|
20
|
+
export function StatusBar({ response, rawText, contentType }: StatusBarProps) {
|
|
21
|
+
const sizeKb = rawText ? `${(rawText.length / 1024).toFixed(1)} KB` : '';
|
|
22
|
+
const duration = response.duration != null ? `${response.duration}ms` : '';
|
|
23
|
+
const hasStatus = response.status != null;
|
|
24
|
+
const hasCopy = Boolean(rawText);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-3 bg-muted/20">
|
|
28
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
29
|
+
{hasStatus && <StatusBadge status={response.status!} />}
|
|
30
|
+
{response.statusText && (
|
|
31
|
+
<span className="text-xs text-muted-foreground truncate">
|
|
32
|
+
{response.statusText}
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
{sizeKb && (
|
|
36
|
+
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">
|
|
37
|
+
{sizeKb}
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
{duration && (
|
|
41
|
+
<span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">
|
|
42
|
+
{duration}
|
|
43
|
+
</span>
|
|
44
|
+
)}
|
|
45
|
+
{contentType && (
|
|
46
|
+
<span className="text-[10px] text-muted-foreground/50 font-mono truncate">
|
|
47
|
+
{contentType}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{hasCopy && (
|
|
52
|
+
<CopyButton
|
|
53
|
+
value={rawText}
|
|
54
|
+
variant="ghost"
|
|
55
|
+
size="sm"
|
|
56
|
+
className="h-6 px-2 text-[10px] text-muted-foreground shrink-0"
|
|
57
|
+
>
|
|
58
|
+
Copy
|
|
59
|
+
</CopyButton>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import type { ViewMode } from './types';
|
|
6
|
+
|
|
7
|
+
interface ViewTabsProps {
|
|
8
|
+
active: ViewMode;
|
|
9
|
+
onChange: (mode: ViewMode) => void;
|
|
10
|
+
/** When false the Preview tab is hidden — only HTML responses get
|
|
11
|
+
* a useful preview, everything else renders the same as Pretty. */
|
|
12
|
+
showPreview: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LABELS: Record<ViewMode, string> = {
|
|
16
|
+
pretty: 'Pretty',
|
|
17
|
+
raw: 'Raw',
|
|
18
|
+
preview: 'Preview',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Tab strip for switching between Pretty / Raw / Preview. Matches the
|
|
22
|
+
* visual weight of the ``LanguageTabs`` in CodeSamples so the page
|
|
23
|
+
* reads as one coherent toolbar system. */
|
|
24
|
+
export function ViewTabs({ active, onChange, showPreview }: ViewTabsProps) {
|
|
25
|
+
const tabs: ViewMode[] = showPreview ? ['pretty', 'raw', 'preview'] : ['pretty', 'raw'];
|
|
26
|
+
return (
|
|
27
|
+
<div className="shrink-0 border-b px-3 py-1.5 flex items-center gap-1">
|
|
28
|
+
{tabs.map((t) => (
|
|
29
|
+
<button
|
|
30
|
+
key={t}
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => onChange(t)}
|
|
33
|
+
className={cn(
|
|
34
|
+
'h-6 px-2.5 rounded text-[11px] font-medium transition-colors',
|
|
35
|
+
active === t
|
|
36
|
+
? 'bg-muted text-foreground'
|
|
37
|
+
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted/50',
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{LABELS[t]}
|
|
41
|
+
</button>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|