@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
|
@@ -13,7 +13,21 @@ interface ApiIntroSectionProps {
|
|
|
13
13
|
resolvedBaseUrl?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface BaseUrlRow {
|
|
17
|
+
url: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }: ApiIntroSectionProps) {
|
|
22
|
+
// Prefer the *resolved* base URL whenever we have one — that's the
|
|
23
|
+
// URL actual requests target, not the raw ``servers[0].url`` from
|
|
24
|
+
// the spec (which can be a bare path like ``/api/v3``). Fall back
|
|
25
|
+
// to the spec's ``servers`` list so specs that document multiple
|
|
26
|
+
// servers keep showing all of them.
|
|
27
|
+
const baseUrlRows: BaseUrlRow[] = resolvedBaseUrl
|
|
28
|
+
? [{ url: resolvedBaseUrl, description: info.servers?.[0]?.description }]
|
|
29
|
+
: (info.servers ?? []).map((s) => ({ url: s.url, description: s.description }));
|
|
30
|
+
|
|
17
31
|
return (
|
|
18
32
|
<section className="pb-10 mb-10 border-b">
|
|
19
33
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
@@ -38,20 +52,20 @@ export function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }: Ap
|
|
|
38
52
|
</div>
|
|
39
53
|
)}
|
|
40
54
|
|
|
41
|
-
{
|
|
55
|
+
{baseUrlRows.length > 0 && (
|
|
42
56
|
<div className="mt-6 space-y-2">
|
|
43
57
|
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
44
58
|
Base URL
|
|
45
59
|
</h4>
|
|
46
60
|
<div className="space-y-1.5">
|
|
47
|
-
{
|
|
48
|
-
<div key={`${
|
|
61
|
+
{baseUrlRows.map((row, i) => (
|
|
62
|
+
<div key={`${row.url}-${i}`} className="flex items-baseline gap-2 flex-wrap">
|
|
49
63
|
<code className="font-mono text-xs px-2 py-1 rounded bg-muted border">
|
|
50
|
-
{
|
|
64
|
+
{row.url}
|
|
51
65
|
</code>
|
|
52
|
-
{
|
|
66
|
+
{row.description && (
|
|
53
67
|
<span className="text-xs text-muted-foreground">
|
|
54
|
-
{
|
|
68
|
+
{row.description}
|
|
55
69
|
</span>
|
|
56
70
|
)}
|
|
57
71
|
</div>
|
|
@@ -2,17 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
|
|
5
|
-
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema } from '../../types';
|
|
5
|
+
import type { ApiEndpoint, LoadedSchemaEntry, OpenApiInfo, OpenApiSchema } from '../../types';
|
|
6
|
+
import {
|
|
7
|
+
getScrollParent,
|
|
8
|
+
getScrollTop,
|
|
9
|
+
getTargetTop,
|
|
10
|
+
getViewportHeight,
|
|
11
|
+
scrollTargetTo,
|
|
12
|
+
type ScrollTarget,
|
|
13
|
+
} from '../../utils/scrollParent';
|
|
6
14
|
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
7
15
|
import { ApiIntroSection } from './ApiIntroSection';
|
|
8
16
|
import { EndpointDoc } from './EndpointDoc';
|
|
9
|
-
import {
|
|
17
|
+
import { useSectionHashRouter } from './EndpointDoc/hooks/useSectionHash';
|
|
18
|
+
import { SchemaCopyMenu } from './SchemaCopyMenu';
|
|
10
19
|
|
|
11
20
|
export interface DocsViewHandle {
|
|
12
21
|
scrollToAnchor: (anchor: string) => void;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
// ─── Props ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
interface SelectorProps {
|
|
27
|
+
grouping?: 'selector';
|
|
16
28
|
info: OpenApiInfo | null;
|
|
17
29
|
rawSchema: OpenApiSchema | null;
|
|
18
30
|
resolvedBaseUrl?: string;
|
|
@@ -20,48 +32,174 @@ interface DocsViewProps {
|
|
|
20
32
|
selectedVersion: string;
|
|
21
33
|
loadedEndpoint: ApiEndpoint | null;
|
|
22
34
|
onTryEndpoint: (ep: ApiEndpoint) => void;
|
|
23
|
-
onActiveChange: (anchor: string | null) => void;
|
|
35
|
+
onActiveChange: (anchor: string | null, schemaId: string | null) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SectionsProps {
|
|
39
|
+
grouping: 'sections';
|
|
40
|
+
/** Per-schema data (info + endpoints). Rendered in order. */
|
|
41
|
+
schemasData: LoadedSchemaEntry[];
|
|
42
|
+
selectedVersion: string;
|
|
43
|
+
loadedEndpoint: ApiEndpoint | null;
|
|
44
|
+
onTryEndpoint: (ep: ApiEndpoint) => void;
|
|
45
|
+
onActiveChange: (anchor: string | null, schemaId: string | null) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type DocsViewProps = SelectorProps | SectionsProps;
|
|
49
|
+
|
|
50
|
+
// ─── View-model types ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
interface EndpointRow {
|
|
53
|
+
key: string;
|
|
54
|
+
endpoint: ApiEndpoint;
|
|
55
|
+
isLoaded: boolean;
|
|
56
|
+
schemaId: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type SectionState =
|
|
60
|
+
| { kind: 'ready'; rows: EndpointRow[] }
|
|
61
|
+
| { kind: 'loading' }
|
|
62
|
+
| { kind: 'error'; message: string }
|
|
63
|
+
| { kind: 'empty' };
|
|
64
|
+
|
|
65
|
+
interface SchemaSectionVM {
|
|
66
|
+
schemaId: string;
|
|
67
|
+
title: string;
|
|
68
|
+
version: string | null;
|
|
69
|
+
description: string | null;
|
|
70
|
+
state: SectionState;
|
|
71
|
+
/** Copy-for-AI payload. ``null`` when the section is still loading
|
|
72
|
+
* or failed — the dropdown stays disabled. */
|
|
73
|
+
rawSchema: OpenApiSchema | null;
|
|
74
|
+
baseUrl: string | undefined;
|
|
75
|
+
allEndpoints: ApiEndpoint[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Pixel offset from the top of the scroll container where the viewer
|
|
81
|
+
* should "park" sections. Reads ``--navbar-height`` for back-compat
|
|
82
|
+
* with pages that already set it; defaults to ``0`` for embedded /
|
|
83
|
+
* no-navbar setups (the common case when hosted in a shell). */
|
|
84
|
+
const readNavbarOffset = () => {
|
|
85
|
+
if (typeof document === 'undefined') return 0;
|
|
86
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue('--navbar-height');
|
|
87
|
+
const parsed = parseInt(raw || '', 10);
|
|
88
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const isSameEndpoint = (a: ApiEndpoint | null, b: ApiEndpoint) =>
|
|
92
|
+
a !== null && a.method === b.method && a.path === b.path;
|
|
93
|
+
|
|
94
|
+
function buildEndpointRow(
|
|
95
|
+
ep: ApiEndpoint,
|
|
96
|
+
loadedEndpoint: ApiEndpoint | null,
|
|
97
|
+
schemaId: string | null,
|
|
98
|
+
): EndpointRow {
|
|
99
|
+
const keySchema = schemaId ? `${schemaId}-` : '';
|
|
100
|
+
return {
|
|
101
|
+
key: `${keySchema}${ep.method}-${ep.path}`,
|
|
102
|
+
endpoint: ep,
|
|
103
|
+
isLoaded: isSameEndpoint(loadedEndpoint, ep),
|
|
104
|
+
schemaId,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildSchemaSectionVM(
|
|
109
|
+
entry: LoadedSchemaEntry,
|
|
110
|
+
selectedVersion: string,
|
|
111
|
+
loadedEndpoint: ApiEndpoint | null,
|
|
112
|
+
): SchemaSectionVM {
|
|
113
|
+
const title = entry.info?.title ?? entry.source.name;
|
|
114
|
+
const version = entry.info?.version ?? null;
|
|
115
|
+
const description = entry.info?.description ?? null;
|
|
116
|
+
|
|
117
|
+
let state: SectionState;
|
|
118
|
+
if (entry.loading) {
|
|
119
|
+
state = { kind: 'loading' };
|
|
120
|
+
} else if (entry.error) {
|
|
121
|
+
state = { kind: 'error', message: entry.error };
|
|
122
|
+
} else {
|
|
123
|
+
const visible = deduplicateEndpoints(entry.endpoints, selectedVersion);
|
|
124
|
+
state = visible.length === 0
|
|
125
|
+
? { kind: 'empty' }
|
|
126
|
+
: {
|
|
127
|
+
kind: 'ready',
|
|
128
|
+
rows: visible.map((ep) => buildEndpointRow(ep, loadedEndpoint, entry.source.id)),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
schemaId: entry.source.id,
|
|
134
|
+
title,
|
|
135
|
+
version,
|
|
136
|
+
description,
|
|
137
|
+
state,
|
|
138
|
+
rawSchema: entry.rawSchema,
|
|
139
|
+
baseUrl: entry.resolvedBaseUrl,
|
|
140
|
+
allEndpoints: entry.endpoints,
|
|
141
|
+
};
|
|
24
142
|
}
|
|
25
143
|
|
|
144
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
26
146
|
export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function DocsView(
|
|
27
|
-
|
|
28
|
-
info,
|
|
29
|
-
rawSchema,
|
|
30
|
-
resolvedBaseUrl,
|
|
31
|
-
endpoints,
|
|
32
|
-
selectedVersion,
|
|
33
|
-
loadedEndpoint,
|
|
34
|
-
onTryEndpoint,
|
|
35
|
-
onActiveChange,
|
|
36
|
-
},
|
|
147
|
+
props,
|
|
37
148
|
ref,
|
|
38
149
|
) {
|
|
39
150
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
151
|
+
const scrollTargetRef = useRef<ScrollTarget | null>(null);
|
|
152
|
+
const { onActiveChange } = props;
|
|
40
153
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
154
|
+
// ``#section=<endpointId>.<sectionId>`` shareable deep-links —
|
|
155
|
+
// opens the referenced section in the store and scrolls it in.
|
|
156
|
+
// Idempotent, attaches a single hashchange listener.
|
|
157
|
+
useSectionHashRouter();
|
|
45
158
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
159
|
+
// Resolve the real scroll container once the ref is attached. In
|
|
160
|
+
// standalone pages that's ``window``; inside an ``overflow-auto``
|
|
161
|
+
// shell (dev playground, modal) it's the wrapping DIV.
|
|
162
|
+
const ensureScrollTarget = useCallback((): ScrollTarget | null => {
|
|
163
|
+
if (scrollTargetRef.current) return scrollTargetRef.current;
|
|
164
|
+
if (!scrollRef.current) return null;
|
|
165
|
+
scrollTargetRef.current = getScrollParent(scrollRef.current);
|
|
166
|
+
return scrollTargetRef.current;
|
|
54
167
|
}, []);
|
|
55
168
|
|
|
169
|
+
// Scroll a given section into view. Works against whichever ancestor
|
|
170
|
+
// actually scrolls — window for standalone, the overflow-auto parent
|
|
171
|
+
// for embedded layouts — so callers don't need to know the difference.
|
|
172
|
+
const scrollToAnchor = useCallback(
|
|
173
|
+
(anchor: string) => {
|
|
174
|
+
const root = scrollRef.current;
|
|
175
|
+
if (!root) return;
|
|
176
|
+
const el = root.querySelector<HTMLElement>(`[data-endpoint-anchor="${anchor}"]`);
|
|
177
|
+
if (!el) return;
|
|
178
|
+
const target = ensureScrollTarget();
|
|
179
|
+
if (!target) return;
|
|
180
|
+
const navbar = readNavbarOffset();
|
|
181
|
+
const top =
|
|
182
|
+
el.getBoundingClientRect().top -
|
|
183
|
+
getTargetTop(target) +
|
|
184
|
+
getScrollTop(target) -
|
|
185
|
+
navbar -
|
|
186
|
+
8;
|
|
187
|
+
scrollTargetTo(target, top);
|
|
188
|
+
},
|
|
189
|
+
[ensureScrollTarget],
|
|
190
|
+
);
|
|
191
|
+
|
|
56
192
|
React.useImperativeHandle(ref, () => ({ scrollToAnchor }), [scrollToAnchor]);
|
|
57
193
|
|
|
58
194
|
// Scrollspy: pick the topmost endpoint section whose top is near the
|
|
59
|
-
// upper
|
|
60
|
-
//
|
|
61
|
-
//
|
|
195
|
+
// upper quarter of the viewport. Listens on the real scroll container
|
|
196
|
+
// (see ``ensureScrollTarget``) because ``scroll`` events on a nested
|
|
197
|
+
// overflow:auto element do NOT bubble up to window.
|
|
62
198
|
useEffect(() => {
|
|
63
199
|
const root = scrollRef.current;
|
|
64
200
|
if (!root) return;
|
|
201
|
+
const target = ensureScrollTarget();
|
|
202
|
+
if (!target) return;
|
|
65
203
|
|
|
66
204
|
let rafId = 0;
|
|
67
205
|
let lastActive: string | null = null;
|
|
@@ -70,20 +208,22 @@ export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function
|
|
|
70
208
|
rafId = 0;
|
|
71
209
|
const sections = root.querySelectorAll<HTMLElement>('[data-endpoint-anchor]');
|
|
72
210
|
if (sections.length === 0) return;
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
211
|
+
const navbar = readNavbarOffset();
|
|
212
|
+
const viewportTop = getTargetTop(target);
|
|
213
|
+
const threshold = viewportTop + navbar + getViewportHeight(target) * 0.25;
|
|
214
|
+
let active: HTMLElement | null = null;
|
|
76
215
|
for (const s of Array.from(sections)) {
|
|
77
216
|
const top = s.getBoundingClientRect().top;
|
|
78
217
|
if (top <= threshold) {
|
|
79
|
-
active = s
|
|
218
|
+
active = s;
|
|
80
219
|
} else {
|
|
81
220
|
break;
|
|
82
221
|
}
|
|
83
222
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
223
|
+
const anchor = active?.dataset.endpointAnchor ?? null;
|
|
224
|
+
if (anchor !== lastActive) {
|
|
225
|
+
lastActive = anchor;
|
|
226
|
+
onActiveChange(anchor, active?.dataset.schemaId || null);
|
|
87
227
|
}
|
|
88
228
|
};
|
|
89
229
|
|
|
@@ -93,15 +233,49 @@ export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function
|
|
|
93
233
|
};
|
|
94
234
|
|
|
95
235
|
compute();
|
|
96
|
-
|
|
236
|
+
target.addEventListener('scroll', onScroll, { passive: true });
|
|
237
|
+
// Resize always bubbles to window — listen there regardless of target.
|
|
238
|
+
window.addEventListener('resize', onScroll, { passive: true });
|
|
97
239
|
return () => {
|
|
98
|
-
|
|
240
|
+
target.removeEventListener('scroll', onScroll);
|
|
241
|
+
window.removeEventListener('resize', onScroll);
|
|
99
242
|
if (rafId) cancelAnimationFrame(rafId);
|
|
100
243
|
};
|
|
101
|
-
}, [
|
|
244
|
+
}, [onActiveChange, ensureScrollTarget, props]);
|
|
245
|
+
|
|
246
|
+
if (props.grouping === 'sections') {
|
|
247
|
+
return <SectionsBody scrollRef={scrollRef} {...props} />;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return <SelectorBody scrollRef={scrollRef} {...props} />;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ─── Selector body (single active schema) ────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function SelectorBody({
|
|
256
|
+
scrollRef,
|
|
257
|
+
info,
|
|
258
|
+
rawSchema,
|
|
259
|
+
resolvedBaseUrl,
|
|
260
|
+
endpoints,
|
|
261
|
+
selectedVersion,
|
|
262
|
+
loadedEndpoint,
|
|
263
|
+
onTryEndpoint,
|
|
264
|
+
}: SelectorProps & { scrollRef: React.RefObject<HTMLDivElement | null> }) {
|
|
265
|
+
const visibleEndpoints = useMemo(
|
|
266
|
+
() => deduplicateEndpoints(endpoints, selectedVersion),
|
|
267
|
+
[endpoints, selectedVersion],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const rows = useMemo<EndpointRow[]>(
|
|
271
|
+
() => visibleEndpoints.map((ep) => buildEndpointRow(ep, loadedEndpoint, ep.schemaId ?? null)),
|
|
272
|
+
[visibleEndpoints, loadedEndpoint],
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const isEmpty = rows.length === 0;
|
|
102
276
|
|
|
103
277
|
return (
|
|
104
|
-
<div ref={scrollRef}
|
|
278
|
+
<div ref={scrollRef}>
|
|
105
279
|
<div className="mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12">
|
|
106
280
|
{info && (
|
|
107
281
|
<ApiIntroSection
|
|
@@ -111,27 +285,131 @@ export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function
|
|
|
111
285
|
resolvedBaseUrl={resolvedBaseUrl}
|
|
112
286
|
/>
|
|
113
287
|
)}
|
|
114
|
-
{
|
|
288
|
+
{isEmpty ? (
|
|
115
289
|
<div className="py-16 text-center text-sm text-muted-foreground">
|
|
116
290
|
No endpoints to display.
|
|
117
291
|
</div>
|
|
118
292
|
) : (
|
|
119
293
|
<div className="divide-y divide-border/60">
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
})}
|
|
294
|
+
{rows.map((row) => (
|
|
295
|
+
<EndpointDoc
|
|
296
|
+
key={row.key}
|
|
297
|
+
endpoint={row.endpoint}
|
|
298
|
+
isLoadedInPlayground={row.isLoaded}
|
|
299
|
+
onTryIt={() => onTryEndpoint(row.endpoint)}
|
|
300
|
+
schemaId={row.schemaId}
|
|
301
|
+
/>
|
|
302
|
+
))}
|
|
132
303
|
</div>
|
|
133
304
|
)}
|
|
134
305
|
</div>
|
|
135
306
|
</div>
|
|
136
307
|
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Sections body (all schemas concatenated) ────────────────────────────────
|
|
311
|
+
|
|
312
|
+
function SectionsBody({
|
|
313
|
+
scrollRef,
|
|
314
|
+
schemasData,
|
|
315
|
+
selectedVersion,
|
|
316
|
+
loadedEndpoint,
|
|
317
|
+
onTryEndpoint,
|
|
318
|
+
}: SectionsProps & { scrollRef: React.RefObject<HTMLDivElement | null> }) {
|
|
319
|
+
const sections = useMemo<SchemaSectionVM[]>(
|
|
320
|
+
() => schemasData.map((e) => buildSchemaSectionVM(e, selectedVersion, loadedEndpoint)),
|
|
321
|
+
[schemasData, selectedVersion, loadedEndpoint],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div ref={scrollRef}>
|
|
326
|
+
<div className="mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12 space-y-16">
|
|
327
|
+
{sections.map((section) => (
|
|
328
|
+
<SchemaSectionView key={section.schemaId} section={section} onTryEndpoint={onTryEndpoint} />
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const SchemaSectionView = React.memo(function SchemaSectionView({
|
|
336
|
+
section,
|
|
337
|
+
onTryEndpoint,
|
|
338
|
+
}: {
|
|
339
|
+
section: SchemaSectionVM;
|
|
340
|
+
onTryEndpoint: (ep: ApiEndpoint) => void;
|
|
341
|
+
}) {
|
|
342
|
+
const canCopy = section.rawSchema !== null && section.allEndpoints.length > 0;
|
|
343
|
+
return (
|
|
344
|
+
<section data-schema-anchor={section.schemaId} className="scroll-mt-20">
|
|
345
|
+
<header className="mb-8 pb-4 border-b">
|
|
346
|
+
<div className="flex items-start justify-between gap-4">
|
|
347
|
+
<div className="flex items-baseline gap-3 min-w-0">
|
|
348
|
+
<h2 className="text-2xl font-semibold tracking-tight">{section.title}</h2>
|
|
349
|
+
{section.version && (
|
|
350
|
+
<span className="font-mono text-xs text-muted-foreground/70">
|
|
351
|
+
v{section.version}
|
|
352
|
+
</span>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
{canCopy && (
|
|
356
|
+
<SchemaCopyMenu
|
|
357
|
+
schema={section.rawSchema}
|
|
358
|
+
endpoints={section.allEndpoints}
|
|
359
|
+
baseUrl={section.baseUrl}
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
{section.description && (
|
|
364
|
+
<p className="mt-2 text-sm text-muted-foreground whitespace-pre-wrap">
|
|
365
|
+
{section.description}
|
|
366
|
+
</p>
|
|
367
|
+
)}
|
|
368
|
+
</header>
|
|
369
|
+
<SchemaSectionStateView section={section} onTryEndpoint={onTryEndpoint} />
|
|
370
|
+
</section>
|
|
371
|
+
);
|
|
137
372
|
});
|
|
373
|
+
|
|
374
|
+
function SchemaSectionStateView({
|
|
375
|
+
section,
|
|
376
|
+
onTryEndpoint,
|
|
377
|
+
}: {
|
|
378
|
+
section: SchemaSectionVM;
|
|
379
|
+
onTryEndpoint: (ep: ApiEndpoint) => void;
|
|
380
|
+
}) {
|
|
381
|
+
switch (section.state.kind) {
|
|
382
|
+
case 'loading':
|
|
383
|
+
return (
|
|
384
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
385
|
+
Loading {section.title}…
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
case 'error':
|
|
389
|
+
return (
|
|
390
|
+
<div className="py-8 text-center text-sm text-destructive">
|
|
391
|
+
Failed to load {section.title}: {section.state.message}
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
case 'empty':
|
|
395
|
+
return (
|
|
396
|
+
<div className="py-8 text-center text-sm text-muted-foreground">
|
|
397
|
+
No endpoints in this schema.
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
case 'ready':
|
|
401
|
+
return (
|
|
402
|
+
<div className="divide-y divide-border/60">
|
|
403
|
+
{section.state.rows.map((row) => (
|
|
404
|
+
<EndpointDoc
|
|
405
|
+
key={row.key}
|
|
406
|
+
endpoint={row.endpoint}
|
|
407
|
+
isLoadedInPlayground={row.isLoaded}
|
|
408
|
+
onTryIt={() => onTryEndpoint(row.endpoint)}
|
|
409
|
+
schemaId={row.schemaId}
|
|
410
|
+
/>
|
|
411
|
+
))}
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { CODE_SAMPLE_TARGETS, type CodeSampleTargetId } from '../../../../utils/codeSamples';
|
|
6
|
+
|
|
7
|
+
interface LanguageTabsProps {
|
|
8
|
+
activeId: CodeSampleTargetId;
|
|
9
|
+
onChange: (id: CodeSampleTargetId) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Horizontal tab strip for switching between cURL/JS/Python/… The
|
|
13
|
+
* strip scrolls horizontally on narrow viewports rather than wrapping
|
|
14
|
+
* to a second line so the adjacent code block keeps its vertical
|
|
15
|
+
* rhythm. */
|
|
16
|
+
export function LanguageTabs({ activeId, onChange }: LanguageTabsProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center gap-1 overflow-x-auto -mx-1 px-1">
|
|
19
|
+
{CODE_SAMPLE_TARGETS.map((t) => (
|
|
20
|
+
<button
|
|
21
|
+
key={t.id}
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => onChange(t.id)}
|
|
24
|
+
className={cn(
|
|
25
|
+
'shrink-0 h-7 px-2.5 rounded text-xs font-medium transition-colors',
|
|
26
|
+
activeId === t.id
|
|
27
|
+
? 'bg-muted text-foreground'
|
|
28
|
+
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted/50',
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{t.label}
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import PrettyCode from '../../../../../PrettyCode';
|
|
4
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
5
|
+
import { useEndpointDocContext } from '../context';
|
|
6
|
+
import { useEndpointDocStore } from '../store';
|
|
7
|
+
import { useActiveCodeTab } from '../store/selectors';
|
|
8
|
+
import { LanguageTabs } from './LanguageTabs';
|
|
9
|
+
import { useCodeSnippet } from './useCodeSnippet';
|
|
10
|
+
|
|
11
|
+
interface CodeSamplesProps {
|
|
12
|
+
endpoint: ApiEndpoint;
|
|
13
|
+
/** Optional body to include in generated snippets. When omitted we
|
|
14
|
+
* use ``endpoint.requestBody?.example`` if present, so the snippet
|
|
15
|
+
* shows a realistic payload out of the box. */
|
|
16
|
+
body?: string;
|
|
17
|
+
/** Parameter values to substitute into the URL. Missing path params
|
|
18
|
+
* fall back to ``{name}`` placeholders so the snippet still
|
|
19
|
+
* illustrates the shape. */
|
|
20
|
+
parameters?: Record<string, string>;
|
|
21
|
+
/** Extra headers to include in snippets (e.g. a picked API key). */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** Base URL override — falls back to ``endpoint.path`` which
|
|
24
|
+
* already has the resolved base URL prepended by the extractor. */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Code samples block: language tab bar + highlighted snippet. The
|
|
29
|
+
* outer Section wrapper (collapsible) lives one level up; this
|
|
30
|
+
* component is always "open" from its own perspective. */
|
|
31
|
+
export function CodeSamples({ endpoint, body, parameters, headers, baseUrl }: CodeSamplesProps) {
|
|
32
|
+
const { endpointId } = useEndpointDocContext();
|
|
33
|
+
const activeId = useActiveCodeTab(endpointId);
|
|
34
|
+
const setCodeTab = useEndpointDocStore((s) => s.setCodeTab);
|
|
35
|
+
|
|
36
|
+
const { snippet, prism } = useCodeSnippet({
|
|
37
|
+
endpoint,
|
|
38
|
+
body,
|
|
39
|
+
parameters,
|
|
40
|
+
headers,
|
|
41
|
+
baseUrl,
|
|
42
|
+
activeId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-2.5">
|
|
47
|
+
<LanguageTabs activeId={activeId} onChange={(id) => setCodeTab(endpointId, id)} />
|
|
48
|
+
<PrettyCode
|
|
49
|
+
data={snippet}
|
|
50
|
+
language={prism as never}
|
|
51
|
+
isCompact
|
|
52
|
+
maxLines={20}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
6
|
+
import {
|
|
7
|
+
CODE_SAMPLE_TARGETS,
|
|
8
|
+
renderSnippet,
|
|
9
|
+
type CodeSampleTargetId,
|
|
10
|
+
} from '../../../../utils/codeSamples';
|
|
11
|
+
import { buildHarRequest } from '../../../../utils/operationToHar';
|
|
12
|
+
import { resolveAbsolute } from '../../../../utils/url';
|
|
13
|
+
|
|
14
|
+
interface UseCodeSnippetInput {
|
|
15
|
+
endpoint: ApiEndpoint;
|
|
16
|
+
body?: string;
|
|
17
|
+
parameters?: Record<string, string>;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
activeId: CodeSampleTargetId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UseCodeSnippetResult {
|
|
24
|
+
/** Fully-rendered snippet string. Always present — falls back to an
|
|
25
|
+
* "unavailable" message if the generator returns null, so the
|
|
26
|
+
* consumer can always mount ``PrettyCode``. */
|
|
27
|
+
snippet: string;
|
|
28
|
+
/** Prism language id matching ``activeId`` — passed to PrettyCode so
|
|
29
|
+
* it picks the right highlighter. */
|
|
30
|
+
prism: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Encapsulates HAR build + snippet render + memoisation for the Code
|
|
34
|
+
* Samples block. Kept as a hook (rather than inline ``useMemo`` blocks
|
|
35
|
+
* in the component) so unit tests can exercise the snippet pipeline
|
|
36
|
+
* independently of React rendering. */
|
|
37
|
+
export function useCodeSnippet({
|
|
38
|
+
endpoint,
|
|
39
|
+
body,
|
|
40
|
+
parameters,
|
|
41
|
+
headers,
|
|
42
|
+
baseUrl,
|
|
43
|
+
activeId,
|
|
44
|
+
}: UseCodeSnippetInput): UseCodeSnippetResult {
|
|
45
|
+
const effectiveBody = body ?? endpoint.requestBody?.example;
|
|
46
|
+
|
|
47
|
+
// Build the HAR once per input change — every tab rebuilds its
|
|
48
|
+
// snippet from this shared request shape.
|
|
49
|
+
//
|
|
50
|
+
// ``endpoint.path`` already carries the schema's ``servers[0].url``
|
|
51
|
+
// (joined upstream in ``useOpenApiSchema``). Usually that's a path
|
|
52
|
+
// like ``/api/v3/pet`` — good enough for same-origin fetch, wrong
|
|
53
|
+
// for curl/python/go which need a runnable absolute URL. We hand
|
|
54
|
+
// off to ``resolveAbsolute`` so the snippet is copy-pasteable from
|
|
55
|
+
// a terminal without the user having to edit the host in manually.
|
|
56
|
+
//
|
|
57
|
+
// Priority: explicit ``baseUrl`` prop > resolved origin > bare path.
|
|
58
|
+
const har = useMemo(() => {
|
|
59
|
+
const h = buildHarRequest({
|
|
60
|
+
endpoint,
|
|
61
|
+
body: effectiveBody,
|
|
62
|
+
parameters,
|
|
63
|
+
headers,
|
|
64
|
+
baseUrl,
|
|
65
|
+
});
|
|
66
|
+
return baseUrl ? h : { ...h, url: resolveAbsolute(h.url) };
|
|
67
|
+
}, [endpoint, effectiveBody, parameters, headers, baseUrl]);
|
|
68
|
+
|
|
69
|
+
return useMemo(() => {
|
|
70
|
+
const target = CODE_SAMPLE_TARGETS.find((t) => t.id === activeId)!;
|
|
71
|
+
const code = renderSnippet(har, activeId);
|
|
72
|
+
return {
|
|
73
|
+
snippet: code ?? `// Snippet for ${target.label} is unavailable for this request.`,
|
|
74
|
+
prism: target.prism,
|
|
75
|
+
};
|
|
76
|
+
}, [har, activeId]);
|
|
77
|
+
}
|