@djangocfg/ui-tools 2.1.289 → 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-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
- package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
- package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
- 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 +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -14
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
- 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 +6 -0
- 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 +8 -2
- 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/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/useOpenApiSchema.ts +41 -71
- package/src/tools/OpenapiViewer/types.ts +10 -0
- 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/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-TKJQ5W5E.mjs.map +0 -1
- package/dist/DocsLayout-YDR7DSMM.cjs.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 -273
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
- 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>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
15
15
|
import { ApiIntroSection } from './ApiIntroSection';
|
|
16
16
|
import { EndpointDoc } from './EndpointDoc';
|
|
17
|
+
import { useSectionHashRouter } from './EndpointDoc/hooks/useSectionHash';
|
|
17
18
|
import { SchemaCopyMenu } from './SchemaCopyMenu';
|
|
18
19
|
|
|
19
20
|
export interface DocsViewHandle {
|
|
@@ -150,6 +151,11 @@ export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function
|
|
|
150
151
|
const scrollTargetRef = useRef<ScrollTarget | null>(null);
|
|
151
152
|
const { onActiveChange } = props;
|
|
152
153
|
|
|
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();
|
|
158
|
+
|
|
153
159
|
// Resolve the real scroll container once the ref is attached. In
|
|
154
160
|
// standalone pages that's ``window``; inside an ``overflow-auto``
|
|
155
161
|
// shell (dev playground, modal) it's the wrapping DIV.
|
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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
ChevronsDownUp,
|
|
6
|
+
ChevronsUpDown,
|
|
7
|
+
FileCode2,
|
|
8
|
+
Link2,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Tooltip,
|
|
14
|
+
TooltipContent,
|
|
15
|
+
TooltipTrigger,
|
|
16
|
+
SafeTooltipProvider,
|
|
17
|
+
} from '@djangocfg/ui-core/components';
|
|
18
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
19
|
+
|
|
20
|
+
import { useEndpointDocContext } from '../context';
|
|
21
|
+
import { sectionKey, useEndpointDocStore } from '../store';
|
|
22
|
+
import type { SectionId } from '../types';
|
|
23
|
+
|
|
24
|
+
interface MetaActionsProps {
|
|
25
|
+
anchor: string;
|
|
26
|
+
endpointMarkdown: string;
|
|
27
|
+
/** Sections present on this endpoint — expand/collapse acts only
|
|
28
|
+
* on visible rows, never on catalogue items the card doesn't render. */
|
|
29
|
+
presentSections: readonly SectionId[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IconButtonProps {
|
|
33
|
+
label: string;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
active?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tight 24×24 icon button used across the meta row. Keeps the hit
|
|
40
|
+
* target compact so the row reads as a secondary metadata strip, not
|
|
41
|
+
* a toolbar that competes with the path for attention. */
|
|
42
|
+
function IconButton({ label, onClick, children, active }: IconButtonProps) {
|
|
43
|
+
return (
|
|
44
|
+
<Tooltip>
|
|
45
|
+
<TooltipTrigger asChild>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
aria-label={label}
|
|
50
|
+
className={cn(
|
|
51
|
+
'shrink-0 h-6 w-6 inline-flex items-center justify-center rounded',
|
|
52
|
+
'text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors',
|
|
53
|
+
active && 'text-emerald-500 hover:text-emerald-500',
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</button>
|
|
58
|
+
</TooltipTrigger>
|
|
59
|
+
<TooltipContent side="bottom" className="text-[11px]">
|
|
60
|
+
{label}
|
|
61
|
+
</TooltipContent>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Inline meta-row actions: copy link · copy markdown · expand/collapse
|
|
67
|
+
* all. Actions are always visible (corporate tool pattern) but sized
|
|
68
|
+
* down so the path on the next line stays the visual focal point. */
|
|
69
|
+
export function MetaActions({ anchor, endpointMarkdown, presentSections }: MetaActionsProps) {
|
|
70
|
+
const { endpointId } = useEndpointDocContext();
|
|
71
|
+
const expandAll = useEndpointDocStore((s) => s.expandAll);
|
|
72
|
+
const collapseAll = useEndpointDocStore((s) => s.collapseAll);
|
|
73
|
+
const openSections = useEndpointDocStore((s) => s.openSections);
|
|
74
|
+
|
|
75
|
+
const [justCopied, setJustCopied] = useState<'link' | 'md' | null>(null);
|
|
76
|
+
const flash = useCallback((which: 'link' | 'md') => {
|
|
77
|
+
setJustCopied(which);
|
|
78
|
+
setTimeout(() => setJustCopied(null), 1200);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const mostlyOpen = useMemo(() => {
|
|
82
|
+
if (presentSections.length === 0) return false;
|
|
83
|
+
let openCount = 0;
|
|
84
|
+
for (const sid of presentSections) {
|
|
85
|
+
if (openSections[sectionKey(endpointId, sid)]) openCount += 1;
|
|
86
|
+
}
|
|
87
|
+
return openCount > presentSections.length / 2;
|
|
88
|
+
}, [openSections, presentSections, endpointId]);
|
|
89
|
+
|
|
90
|
+
const copyLink = useCallback(() => {
|
|
91
|
+
if (typeof window === 'undefined') return;
|
|
92
|
+
const url = `${window.location.origin}${window.location.pathname}#${anchor}`;
|
|
93
|
+
void navigator.clipboard?.writeText(url).then(() => flash('link'));
|
|
94
|
+
}, [anchor, flash]);
|
|
95
|
+
|
|
96
|
+
const copyMarkdown = useCallback(() => {
|
|
97
|
+
if (typeof window === 'undefined') return;
|
|
98
|
+
void navigator.clipboard?.writeText(endpointMarkdown).then(() => flash('md'));
|
|
99
|
+
}, [endpointMarkdown, flash]);
|
|
100
|
+
|
|
101
|
+
const toggleAll = useCallback(() => {
|
|
102
|
+
if (mostlyOpen) collapseAll(endpointId, presentSections);
|
|
103
|
+
else expandAll(endpointId, presentSections);
|
|
104
|
+
}, [mostlyOpen, collapseAll, expandAll, endpointId, presentSections]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<SafeTooltipProvider delayDuration={200}>
|
|
108
|
+
<div className="flex items-center gap-0.5">
|
|
109
|
+
<IconButton
|
|
110
|
+
label={justCopied === 'link' ? 'Copied!' : 'Copy link to endpoint'}
|
|
111
|
+
onClick={copyLink}
|
|
112
|
+
active={justCopied === 'link'}
|
|
113
|
+
>
|
|
114
|
+
{justCopied === 'link' ? (
|
|
115
|
+
<Check className="h-3.5 w-3.5" />
|
|
116
|
+
) : (
|
|
117
|
+
<Link2 className="h-3.5 w-3.5" />
|
|
118
|
+
)}
|
|
119
|
+
</IconButton>
|
|
120
|
+
<IconButton
|
|
121
|
+
label={justCopied === 'md' ? 'Copied!' : 'Copy as Markdown (for AI)'}
|
|
122
|
+
onClick={copyMarkdown}
|
|
123
|
+
active={justCopied === 'md'}
|
|
124
|
+
>
|
|
125
|
+
{justCopied === 'md' ? (
|
|
126
|
+
<Check className="h-3.5 w-3.5" />
|
|
127
|
+
) : (
|
|
128
|
+
<FileCode2 className="h-3.5 w-3.5" />
|
|
129
|
+
)}
|
|
130
|
+
</IconButton>
|
|
131
|
+
{presentSections.length >= 2 && (
|
|
132
|
+
<IconButton
|
|
133
|
+
label={mostlyOpen ? 'Collapse all sections' : 'Expand all sections'}
|
|
134
|
+
onClick={toggleAll}
|
|
135
|
+
>
|
|
136
|
+
{mostlyOpen ? (
|
|
137
|
+
<ChevronsDownUp className="h-3.5 w-3.5" />
|
|
138
|
+
) : (
|
|
139
|
+
<ChevronsUpDown className="h-3.5 w-3.5" />
|
|
140
|
+
)}
|
|
141
|
+
</IconButton>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</SafeTooltipProvider>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Thin re-export so the Header barrel file stays self-contained. The
|
|
4
|
+
// actual badge lives in ``shared/ui`` because it's used by the sidebar
|
|
5
|
+
// and the playground too — duplicating styles would drift.
|
|
6
|
+
export { MethodBadge } from '../../../shared/ui';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { relativePath } from '../../../shared/ui';
|
|
4
|
+
|
|
5
|
+
interface PathDisplayProps {
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The endpoint path as the visual focal point of the card. Renders in
|
|
10
|
+
* large monospace so readers scanning the page land on it first — the
|
|
11
|
+
* path is what distinguishes one endpoint from another, so it should
|
|
12
|
+
* be the biggest visible element short of the section title hierarchy.
|
|
13
|
+
*
|
|
14
|
+
* ``relativePath`` strips the resolved base URL so the docs always
|
|
15
|
+
* show the API-local path, not an absolute URL cluttered with host
|
|
16
|
+
* noise. */
|
|
17
|
+
export function PathDisplay({ path }: PathDisplayProps) {
|
|
18
|
+
return (
|
|
19
|
+
<code
|
|
20
|
+
className="block font-mono text-lg md:text-xl font-semibold text-foreground leading-tight"
|
|
21
|
+
style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}
|
|
22
|
+
>
|
|
23
|
+
{relativePath(path)}
|
|
24
|
+
</code>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Play } from 'lucide-react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
|
|
8
|
+
import { MarkdownMessage } from '../../../../../../components/markdown';
|
|
9
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
10
|
+
import { endpointToMarkdown } from '../../../../utils/schemaExport';
|
|
11
|
+
import type { SectionId } from '../types';
|
|
12
|
+
import { MetaActions } from './MetaActions';
|
|
13
|
+
import { MethodBadge } from './MethodBadge';
|
|
14
|
+
import { PathDisplay } from './PathDisplay';
|
|
15
|
+
|
|
16
|
+
interface EndpointHeaderProps {
|
|
17
|
+
endpoint: ApiEndpoint;
|
|
18
|
+
anchor: string;
|
|
19
|
+
isLoadedInPlayground: boolean;
|
|
20
|
+
onTryIt: () => void;
|
|
21
|
+
/** Sections actually rendered on this endpoint — drives the
|
|
22
|
+
* expand/collapse-all icon in the meta row. */
|
|
23
|
+
presentSections: readonly SectionId[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Card header, three stacked rows:
|
|
27
|
+
* 1. Meta — small utility strip: method badge · inline actions · Try-it.
|
|
28
|
+
* 2. Path — large monospace, the visual focus of the card.
|
|
29
|
+
* 3. Description — prose, aligned to the left edge under path.
|
|
30
|
+
*
|
|
31
|
+
* Splitting metadata from the path lets the path itself become the
|
|
32
|
+
* focal point — readers scan paths when scrolling, not badges. The
|
|
33
|
+
* meta row stays compact so it reads as "info about this endpoint"
|
|
34
|
+
* rather than "toolbar that competes for attention". */
|
|
35
|
+
export function EndpointHeader({
|
|
36
|
+
endpoint,
|
|
37
|
+
anchor,
|
|
38
|
+
isLoadedInPlayground,
|
|
39
|
+
onTryIt,
|
|
40
|
+
presentSections,
|
|
41
|
+
}: EndpointHeaderProps) {
|
|
42
|
+
// Memoise the markdown dump — only recomputes when the endpoint
|
|
43
|
+
// reference changes, not on unrelated re-renders of the subtree.
|
|
44
|
+
const endpointMd = useMemo(() => endpointToMarkdown(endpoint), [endpoint]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<header className="space-y-3">
|
|
48
|
+
{/* Row 1 — meta strip. Badge + inline icon actions on the
|
|
49
|
+
left, primary CTA on the right. Kept tight (24px tall)
|
|
50
|
+
so it doesn't visually compete with the path row below. */}
|
|
51
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
52
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
53
|
+
<MethodBadge method={endpoint.method} />
|
|
54
|
+
<MetaActions
|
|
55
|
+
anchor={anchor}
|
|
56
|
+
endpointMarkdown={endpointMd}
|
|
57
|
+
presentSections={presentSections}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<Button
|
|
61
|
+
size="sm"
|
|
62
|
+
variant={isLoadedInPlayground ? 'secondary' : 'default'}
|
|
63
|
+
onClick={onTryIt}
|
|
64
|
+
className="ml-auto h-7 text-xs gap-1.5 px-2.5"
|
|
65
|
+
>
|
|
66
|
+
<Play className="h-3 w-3" />
|
|
67
|
+
{isLoadedInPlayground ? 'Loaded' : 'Try it'}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Row 2 — path as the visual focal point. Larger and more
|
|
72
|
+
prominent than it was when competing with the badge
|
|
73
|
+
and action icons inline. */}
|
|
74
|
+
<div className="min-w-0">
|
|
75
|
+
<PathDisplay path={endpoint.path} />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Row 3 — description, aligned to the left edge under the
|
|
79
|
+
path. Text-sm so it reads as subtitle, not body copy. */}
|
|
80
|
+
{endpoint.description && (
|
|
81
|
+
<div className="text-muted-foreground text-sm">
|
|
82
|
+
<MarkdownMessage content={endpoint.description} />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</header>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
4
|
+
import { ParamRow } from './ParamRow';
|
|
5
|
+
|
|
6
|
+
interface ParamGroupProps {
|
|
7
|
+
/** Small uppercase label shown above the group (``Path``, ``Query``). */
|
|
8
|
+
label: string;
|
|
9
|
+
params: NonNullable<ApiEndpoint['parameters']>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Labelled block of parameter rows. We split path vs query so the user
|
|
13
|
+
* can tell which values go in the URL template vs the query string at
|
|
14
|
+
* a glance — both are ``parameters`` in OpenAPI terms but they land in
|
|
15
|
+
* different places in the generated request. */
|
|
16
|
+
export function ParamGroup({ label, params }: ParamGroupProps) {
|
|
17
|
+
if (params.length === 0) return null;
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-1.5">
|
|
20
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.1em] text-muted-foreground/60 px-1">
|
|
21
|
+
{label}
|
|
22
|
+
</div>
|
|
23
|
+
<div className="divide-y border rounded-md overflow-hidden">
|
|
24
|
+
{params.map((p) => (
|
|
25
|
+
<ParamRow key={`${label}-${p.name}`} param={p} />
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
4
|
+
|
|
5
|
+
type Param = NonNullable<ApiEndpoint['parameters']>[number];
|
|
6
|
+
|
|
7
|
+
interface ParamRowProps {
|
|
8
|
+
param: Param;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Single parameter row. Name + required marker + type + (optional)
|
|
12
|
+
* description. Wrapped in ``<div>`` rather than a table row so the
|
|
13
|
+
* list degrades to a stack on narrow widths without losing semantics. */
|
|
14
|
+
export function ParamRow({ param }: ParamRowProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="px-3 py-2.5 bg-background space-y-1">
|
|
17
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
18
|
+
<code className="font-mono text-xs font-medium text-foreground">{param.name}</code>
|
|
19
|
+
{param.required && (
|
|
20
|
+
<span
|
|
21
|
+
title="Required"
|
|
22
|
+
className="text-[9px] text-destructive font-bold leading-none"
|
|
23
|
+
>
|
|
24
|
+
*
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
<code className="font-mono text-[11px] text-muted-foreground/70">{param.type}</code>
|
|
28
|
+
</div>
|
|
29
|
+
{param.description && (
|
|
30
|
+
<p className="text-xs text-muted-foreground leading-relaxed break-words">
|
|
31
|
+
{param.description}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
4
|
+
import { ParamGroup } from './ParamGroup';
|
|
5
|
+
|
|
6
|
+
interface ParametersProps {
|
|
7
|
+
pathParams: NonNullable<ApiEndpoint['parameters']>;
|
|
8
|
+
queryParams: NonNullable<ApiEndpoint['parameters']>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Combined path + query parameters block. Each group renders only
|
|
12
|
+
* when non-empty; when both are empty the parent should not render
|
|
13
|
+
* the Section at all (we check up-stream so the Section badge shows a
|
|
14
|
+
* real count). */
|
|
15
|
+
export function Parameters({ pathParams, queryParams }: ParametersProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
<ParamGroup label="Path" params={pathParams} />
|
|
19
|
+
<ParamGroup label="Query" params={queryParams} />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
4
|
+
import { SchemaFields } from '../SchemaFields';
|
|
5
|
+
|
|
6
|
+
interface RequestBodyProps {
|
|
7
|
+
body: NonNullable<ApiEndpoint['requestBody']>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Request body preview. Shows a short type summary line (``object``,
|
|
11
|
+
* ``array<object>``, …) alongside the body description, then the
|
|
12
|
+
* tree-view of fields. The example payload is rendered below in its
|
|
13
|
+
* own section (code samples) because the user often wants to copy it
|
|
14
|
+
* independently from inspecting the shape. */
|
|
15
|
+
export function RequestBody({ body }: RequestBodyProps) {
|
|
16
|
+
const typeLabel = body.schema
|
|
17
|
+
? body.type === 'array'
|
|
18
|
+
? `array<${(body.schema as { items?: { type?: string } }).items?.type ?? 'object'}>`
|
|
19
|
+
: body.type
|
|
20
|
+
: body.type;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
<div className="flex items-baseline gap-2 flex-wrap">
|
|
25
|
+
<code className="font-mono text-[11px] text-muted-foreground/80">{typeLabel}</code>
|
|
26
|
+
{body.description && (
|
|
27
|
+
<span className="text-xs text-muted-foreground">{body.description}</span>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
{body.schema && <SchemaFields schema={body.schema} />}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|