@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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ContentKind, DetectedContent } from './types';
|
|
2
|
+
|
|
3
|
+
/** Strip Content-Type parameters (``; charset=utf-8`` etc.) and lower-case. */
|
|
4
|
+
function normaliseContentType(raw: string | null): string | null {
|
|
5
|
+
if (!raw) return null;
|
|
6
|
+
const semi = raw.indexOf(';');
|
|
7
|
+
return (semi === -1 ? raw : raw.slice(0, semi)).trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Pull Content-Type out of the headers bag. We accept both plain
|
|
11
|
+
* objects (``{ "content-type": "..." }``) and ``Headers``-like shapes
|
|
12
|
+
* with a ``.get`` method. The context type is ``any`` so we can't be
|
|
13
|
+
* stricter without plumbing a proper type through the response code
|
|
14
|
+
* path. */
|
|
15
|
+
function readContentType(headers: unknown): string | null {
|
|
16
|
+
if (!headers) return null;
|
|
17
|
+
// Headers instance — use ``.get`` (case-insensitive).
|
|
18
|
+
if (typeof (headers as Headers).get === 'function') {
|
|
19
|
+
return (headers as Headers).get('content-type');
|
|
20
|
+
}
|
|
21
|
+
// Plain object — look up with case-insensitive key match so both
|
|
22
|
+
// ``Content-Type`` and ``content-type`` work.
|
|
23
|
+
if (typeof headers === 'object') {
|
|
24
|
+
for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
|
|
25
|
+
if (k.toLowerCase() === 'content-type') {
|
|
26
|
+
return typeof v === 'string' ? v : null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Map a normalised MIME type to our short ``ContentKind``. Unknown
|
|
34
|
+
* types fall back to ``text`` so PrettyCode still shows something
|
|
35
|
+
* sensible (plain text is a valid Prism language). */
|
|
36
|
+
function kindFromContentType(mime: string | null): ContentKind {
|
|
37
|
+
if (!mime) return 'text';
|
|
38
|
+
if (mime === 'application/json' || mime.endsWith('+json')) return 'json';
|
|
39
|
+
if (mime === 'text/html' || mime === 'application/xhtml+xml') return 'html';
|
|
40
|
+
if (
|
|
41
|
+
mime === 'application/xml' ||
|
|
42
|
+
mime === 'text/xml' ||
|
|
43
|
+
mime.endsWith('+xml')
|
|
44
|
+
) return 'xml';
|
|
45
|
+
if (mime === 'text/css') return 'css';
|
|
46
|
+
if (
|
|
47
|
+
mime === 'application/javascript' ||
|
|
48
|
+
mime === 'text/javascript' ||
|
|
49
|
+
mime === 'application/x-javascript'
|
|
50
|
+
) return 'javascript';
|
|
51
|
+
return 'text';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Heuristic fallback when ``Content-Type`` is missing or opaque.
|
|
55
|
+
* Returns ``null`` when nothing firm can be inferred; the caller
|
|
56
|
+
* then defaults to ``text``. */
|
|
57
|
+
function kindFromBody(body: string): ContentKind | null {
|
|
58
|
+
const trimmed = body.trimStart();
|
|
59
|
+
if (!trimmed) return null;
|
|
60
|
+
if (trimmed.startsWith('<!DOCTYPE') || /^<html[\s>]/i.test(trimmed)) return 'html';
|
|
61
|
+
if (trimmed.startsWith('<?xml')) return 'xml';
|
|
62
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
63
|
+
// Cheap validity check — full parse happens in ResponsePanel.
|
|
64
|
+
try { JSON.parse(trimmed); return 'json'; } catch { /* fall through */ }
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const PRISM_BY_KIND: Record<ContentKind, DetectedContent['prism']> = {
|
|
70
|
+
json: 'json',
|
|
71
|
+
// ``markup`` is Prism's HTML/XML grammar — there isn't a separate
|
|
72
|
+
// ``html`` language. XML piggy-backs on the same tokeniser.
|
|
73
|
+
html: 'markup',
|
|
74
|
+
xml: 'markup',
|
|
75
|
+
css: 'css',
|
|
76
|
+
javascript: 'javascript',
|
|
77
|
+
text: 'markup',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Detect content kind from headers, falling back to body sniffing
|
|
81
|
+
* when the header is missing. Pure function — safe to call inside
|
|
82
|
+
* ``useMemo`` deps on the raw body and headers. */
|
|
83
|
+
export function detectContent(headers: unknown, rawBody: string): DetectedContent {
|
|
84
|
+
const contentType = normaliseContentType(readContentType(headers));
|
|
85
|
+
const headerKind = kindFromContentType(contentType);
|
|
86
|
+
|
|
87
|
+
// If headers say it's text/plain but the body looks like HTML or
|
|
88
|
+
// JSON, trust the body — a lot of framework 500 pages claim
|
|
89
|
+
// text/html but some setups default to text/plain for errors.
|
|
90
|
+
const kind = headerKind === 'text' ? (kindFromBody(rawBody) ?? 'text') : headerKind;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
kind,
|
|
94
|
+
prism: PRISM_BY_KIND[kind],
|
|
95
|
+
contentType,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2, Send, Terminal, WifiOff } from 'lucide-react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { usePlaygroundContext } from '../../../context/PlaygroundContext';
|
|
7
|
+
import { EmptyState, ScrollArea } from '../ui';
|
|
8
|
+
import { PreviewView } from './PreviewView';
|
|
9
|
+
import { PrettyView } from './PrettyView';
|
|
10
|
+
import { RawView } from './RawView';
|
|
11
|
+
import { StatusBar } from './StatusBar';
|
|
12
|
+
import type { ViewMode } from './types';
|
|
13
|
+
import { useResponseView } from './useResponseView';
|
|
14
|
+
import { ViewTabs } from './ViewTabs';
|
|
15
|
+
|
|
16
|
+
/** Response panel used by both the sticky ``SlideInPlayground`` and the
|
|
17
|
+
* modal ``TryItSheet``. Responsibilities:
|
|
18
|
+
* - Empty / loading / network-error gates
|
|
19
|
+
* - Three view modes (Pretty / Raw / Preview) with a tab bar
|
|
20
|
+
* - Auto-pick the best default view per response type (JSON → Pretty,
|
|
21
|
+
* HTML → Preview, everything else → Pretty as the widest-purpose)
|
|
22
|
+
*
|
|
23
|
+
* View mode is local component state rather than context state — it
|
|
24
|
+
* should reset when the selected endpoint changes and not leak between
|
|
25
|
+
* endpoints. */
|
|
26
|
+
export function ResponsePanel() {
|
|
27
|
+
const { state } = usePlaygroundContext();
|
|
28
|
+
const { response, loading, selectedEndpoint } = state;
|
|
29
|
+
|
|
30
|
+
const { treeData, rawText, detected } = useResponseView(response?.data, response?.headers);
|
|
31
|
+
const showPreview = detected.kind === 'html';
|
|
32
|
+
|
|
33
|
+
// Default view heuristic: show HTML pages preview-first so the
|
|
34
|
+
// reader sees the actual rendered server page immediately.
|
|
35
|
+
// Everything else lands on Pretty.
|
|
36
|
+
const [mode, setMode] = useState<ViewMode>(showPreview ? 'preview' : 'pretty');
|
|
37
|
+
|
|
38
|
+
// Reset the mode when the endpoint or response changes so a
|
|
39
|
+
// previously-selected tab from a different shape doesn't stick
|
|
40
|
+
// around (e.g. stuck on Preview when switching from HTML→JSON).
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setMode(showPreview ? 'preview' : 'pretty');
|
|
43
|
+
}, [selectedEndpoint, response, showPreview]);
|
|
44
|
+
|
|
45
|
+
if (loading) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex items-center justify-center h-full gap-2">
|
|
48
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
49
|
+
<span className="text-xs text-muted-foreground">Sending…</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!selectedEndpoint) return <EmptyState icon={Terminal} text="Response will appear here" />;
|
|
55
|
+
if (!response) return <EmptyState icon={Send} text='Press "Send Request" to see the response' />;
|
|
56
|
+
|
|
57
|
+
const hasError = Boolean(response.error);
|
|
58
|
+
const hasStatus = response.status != null;
|
|
59
|
+
|
|
60
|
+
// Pure network error (no HTTP response at all — CORS, offline, timeout)
|
|
61
|
+
if (hasError && !hasStatus) {
|
|
62
|
+
return (
|
|
63
|
+
<EmptyState
|
|
64
|
+
icon={WifiOff}
|
|
65
|
+
text={response.error!}
|
|
66
|
+
className="text-destructive [&_svg]:text-destructive"
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<StatusBar response={response} rawText={rawText} contentType={detected.contentType} />
|
|
74
|
+
|
|
75
|
+
{/* HTTP-level error body (4xx/5xx — has status but also error flag) */}
|
|
76
|
+
{hasError && (
|
|
77
|
+
<div className="shrink-0 mx-4 mt-3 rounded border border-destructive/20 bg-destructive/5 px-3 py-2">
|
|
78
|
+
<p className="text-xs text-destructive">{response.error}</p>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<ViewTabs active={mode} onChange={setMode} showPreview={showPreview} />
|
|
83
|
+
|
|
84
|
+
<ScrollArea>
|
|
85
|
+
{mode === 'pretty' && (
|
|
86
|
+
<PrettyView treeData={treeData} rawText={rawText} detected={detected} />
|
|
87
|
+
)}
|
|
88
|
+
{mode === 'raw' && <RawView rawText={rawText} />}
|
|
89
|
+
{mode === 'preview' && <PreviewView html={rawText} />}
|
|
90
|
+
</ScrollArea>
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Language } from '../../../../PrettyCode';
|
|
2
|
+
|
|
3
|
+
/** Panels the user can switch between in the response view.
|
|
4
|
+
*
|
|
5
|
+
* - ``pretty``: structured rendering — JsonTree for JSON, syntax-
|
|
6
|
+
* highlighted code for HTML/XML/CSS/text.
|
|
7
|
+
* - ``raw``: plain ``<pre>`` dump of whatever the server returned.
|
|
8
|
+
* - ``preview``: sandboxed iframe for HTML responses (when you want
|
|
9
|
+
* to *see* an error page the server produced rather
|
|
10
|
+
* than read its source). Tab is hidden for non-HTML. */
|
|
11
|
+
export type ViewMode = 'pretty' | 'raw' | 'preview';
|
|
12
|
+
|
|
13
|
+
/** Content categories derived from the server's ``Content-Type`` header.
|
|
14
|
+
* We project the rich MIME type space onto a short enum so the renderer
|
|
15
|
+
* and tab-list only have to switch on a handful of values. */
|
|
16
|
+
export type ContentKind = 'json' | 'html' | 'xml' | 'css' | 'javascript' | 'text';
|
|
17
|
+
|
|
18
|
+
export interface DetectedContent {
|
|
19
|
+
kind: ContentKind;
|
|
20
|
+
/** Prism ``Language`` id matching ``kind`` — fed into PrettyCode. */
|
|
21
|
+
prism: Language;
|
|
22
|
+
/** Full Content-Type header value, stripped of parameters (e.g.
|
|
23
|
+
* ``application/json`` without ``; charset=utf-8``). ``null`` when
|
|
24
|
+
* the server didn't send one. */
|
|
25
|
+
contentType: string | null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { detectContent } from './detectContent';
|
|
4
|
+
import type { DetectedContent } from './types';
|
|
5
|
+
|
|
6
|
+
interface UseResponseViewResult {
|
|
7
|
+
/** Parsed JSON value when the body is JSON, otherwise ``null``.
|
|
8
|
+
* JsonTree needs a live object to render properly, so we parse
|
|
9
|
+
* once up-front and cache. */
|
|
10
|
+
treeData: unknown;
|
|
11
|
+
/** Stringified body, always present when the server returned
|
|
12
|
+
* anything. Used by Raw and Pretty-for-text tabs. */
|
|
13
|
+
rawText: string;
|
|
14
|
+
/** Content-type + prism language inference. */
|
|
15
|
+
detected: DetectedContent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shape the raw response payload into the pieces each view tab
|
|
19
|
+
* needs. Exposed as a hook so the component stays thin and the
|
|
20
|
+
* normalisation is easy to test in isolation. */
|
|
21
|
+
export function useResponseView(
|
|
22
|
+
data: unknown,
|
|
23
|
+
headers: unknown,
|
|
24
|
+
): UseResponseViewResult {
|
|
25
|
+
return useMemo(() => {
|
|
26
|
+
if (data == null) {
|
|
27
|
+
return {
|
|
28
|
+
treeData: null,
|
|
29
|
+
rawText: '',
|
|
30
|
+
detected: detectContent(headers, ''),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// String body — might be JSON-in-a-string or a plain text dump.
|
|
35
|
+
if (typeof data === 'string') {
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
treeData: JSON.parse(data),
|
|
39
|
+
rawText: data,
|
|
40
|
+
detected: detectContent(headers, data),
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
treeData: null,
|
|
45
|
+
rawText: data,
|
|
46
|
+
detected: detectContent(headers, data),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Object body — axios already parsed it. Stringify for Raw/
|
|
52
|
+
// Preview tabs and treat it as JSON regardless of headers.
|
|
53
|
+
const stringified = (() => {
|
|
54
|
+
try { return JSON.stringify(data, null, 2); } catch { return String(data); }
|
|
55
|
+
})();
|
|
56
|
+
return {
|
|
57
|
+
treeData: data,
|
|
58
|
+
rawText: stringified,
|
|
59
|
+
detected: detectContent(headers, stringified),
|
|
60
|
+
};
|
|
61
|
+
}, [data, headers]);
|
|
62
|
+
}
|
|
@@ -5,4 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { default as useOpenApiSchema } from './useOpenApiSchema';
|
|
8
|
-
export { useMobile } from './useMobile';
|
|
8
|
+
export { useMobile } from './useMobile';
|
|
9
|
+
export { useDocsUrlSync, parseDocsHash, buildDocsHash } from './useDocsUrlSync';
|
|
10
|
+
export type { ParsedHash } from './useDocsUrlSync';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
/** Hash format: ``#<schemaId>/<anchor>``.
|
|
6
|
+
* - ``#catalog/ep-get-users`` — specific endpoint in ``catalog`` schema
|
|
7
|
+
* - ``#catalog`` — open ``catalog`` schema at its top
|
|
8
|
+
* - empty — no initial target, leave viewer at its default
|
|
9
|
+
*
|
|
10
|
+
* We intentionally keep this opinionated and stringly-typed: the host app
|
|
11
|
+
* already controls which schemas exist, so there is no room for ambiguity
|
|
12
|
+
* beyond the two segments. */
|
|
13
|
+
export interface ParsedHash {
|
|
14
|
+
schemaId: string | null;
|
|
15
|
+
anchor: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseDocsHash(hash: string): ParsedHash {
|
|
19
|
+
const raw = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
20
|
+
if (!raw) return { schemaId: null, anchor: null };
|
|
21
|
+
const [schemaId = null, ...rest] = raw.split('/');
|
|
22
|
+
const anchor = rest.length > 0 ? rest.join('/') : null;
|
|
23
|
+
return {
|
|
24
|
+
schemaId: schemaId || null,
|
|
25
|
+
anchor: anchor || null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildDocsHash(schemaId: string | null, anchor: string | null): string {
|
|
30
|
+
if (!schemaId && !anchor) return '';
|
|
31
|
+
if (schemaId && anchor) return `#${schemaId}/${anchor}`;
|
|
32
|
+
if (schemaId) return `#${schemaId}`;
|
|
33
|
+
return anchor ? `#${anchor}` : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface UseDocsUrlSyncProps {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
currentSchemaId: string | null;
|
|
39
|
+
activeAnchor: string | null;
|
|
40
|
+
/** Called on mount / ``popstate`` / ``hashchange`` with the hash state.
|
|
41
|
+
* The consumer is responsible for dispatching into its own handlers
|
|
42
|
+
* (switching schema, scrolling to endpoint) in the right order. */
|
|
43
|
+
onHashTarget: (target: ParsedHash) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Two-way sync between browser hash and docs viewer state.
|
|
47
|
+
*
|
|
48
|
+
* - Writes use ``history.replaceState`` so scrollspy-driven updates don't
|
|
49
|
+
* pollute the back/forward stack. User-initiated navigation (click on
|
|
50
|
+
* sidebar row, schema switch) still lands in history because the click
|
|
51
|
+
* itself already did ``pushState`` — or will, via plain anchor hrefs.
|
|
52
|
+
* - Reads happen on mount (initial target) and on ``popstate`` /
|
|
53
|
+
* ``hashchange`` (Back/Forward / external anchor clicks).
|
|
54
|
+
* - When ``enabled`` is false, the hook is a no-op — the viewer stays
|
|
55
|
+
* hash-free so you can embed it inside a larger page. */
|
|
56
|
+
export function useDocsUrlSync({
|
|
57
|
+
enabled,
|
|
58
|
+
currentSchemaId,
|
|
59
|
+
activeAnchor,
|
|
60
|
+
onHashTarget,
|
|
61
|
+
}: UseDocsUrlSyncProps) {
|
|
62
|
+
// Ignore the very first write — otherwise on mount we'd clobber the
|
|
63
|
+
// incoming hash with the viewer's empty defaults before ``onHashTarget``
|
|
64
|
+
// has a chance to apply it.
|
|
65
|
+
const primedRef = useRef(false);
|
|
66
|
+
const onHashTargetRef = useRef(onHashTarget);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
onHashTargetRef.current = onHashTarget;
|
|
69
|
+
}, [onHashTarget]);
|
|
70
|
+
|
|
71
|
+
// Read: mount + hashchange/popstate
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!enabled || typeof window === 'undefined') return;
|
|
74
|
+
|
|
75
|
+
const apply = () => {
|
|
76
|
+
onHashTargetRef.current(parseDocsHash(window.location.hash));
|
|
77
|
+
};
|
|
78
|
+
apply();
|
|
79
|
+
primedRef.current = true;
|
|
80
|
+
|
|
81
|
+
window.addEventListener('hashchange', apply);
|
|
82
|
+
window.addEventListener('popstate', apply);
|
|
83
|
+
return () => {
|
|
84
|
+
window.removeEventListener('hashchange', apply);
|
|
85
|
+
window.removeEventListener('popstate', apply);
|
|
86
|
+
};
|
|
87
|
+
}, [enabled]);
|
|
88
|
+
|
|
89
|
+
// Write: whenever the viewer's state changes
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!enabled || typeof window === 'undefined') return;
|
|
92
|
+
if (!primedRef.current) return;
|
|
93
|
+
|
|
94
|
+
const next = buildDocsHash(currentSchemaId, activeAnchor);
|
|
95
|
+
const current = window.location.hash;
|
|
96
|
+
if (next === current) return;
|
|
97
|
+
|
|
98
|
+
// replaceState keeps Back/Forward meaningful — a single scroll-through
|
|
99
|
+
// the page shouldn't create 50 history entries.
|
|
100
|
+
const url = next
|
|
101
|
+
? `${window.location.pathname}${window.location.search}${next}`
|
|
102
|
+
: `${window.location.pathname}${window.location.search}`;
|
|
103
|
+
window.history.replaceState(window.history.state, '', url);
|
|
104
|
+
}, [enabled, currentSchemaId, activeAnchor]);
|
|
105
|
+
|
|
106
|
+
const pushTarget = useCallback(
|
|
107
|
+
(schemaId: string | null, anchor: string | null) => {
|
|
108
|
+
if (!enabled || typeof window === 'undefined') return;
|
|
109
|
+
const next = buildDocsHash(schemaId, anchor);
|
|
110
|
+
const url = next
|
|
111
|
+
? `${window.location.pathname}${window.location.search}${next}`
|
|
112
|
+
: `${window.location.pathname}${window.location.search}`;
|
|
113
|
+
window.history.pushState(window.history.state, '', url);
|
|
114
|
+
},
|
|
115
|
+
[enabled],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return { pushTarget };
|
|
119
|
+
}
|