@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,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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
import JsonTree from '../../../../../JsonTree';
|
|
8
|
+
|
|
9
|
+
// Compact, read-only JsonTree config. Docs longread is space-constrained
|
|
10
|
+
// so we ship less chrome than the interactive ``ResponsePanel`` — copy
|
|
11
|
+
// button lives outside the tree (on the row header) and collection-info
|
|
12
|
+
// stays on so long arrays don't silently truncate.
|
|
13
|
+
const EXAMPLE_JSON_TREE_CONFIG = {
|
|
14
|
+
maxAutoExpandDepth: 2,
|
|
15
|
+
maxAutoExpandArrayItems: 5,
|
|
16
|
+
maxAutoExpandObjectKeys: 8,
|
|
17
|
+
maxStringLength: 160,
|
|
18
|
+
collectionLimit: 25,
|
|
19
|
+
showCollectionInfo: true,
|
|
20
|
+
showExpandControls: false,
|
|
21
|
+
showActionButtons: false,
|
|
22
|
+
preserveKeyOrder: true,
|
|
23
|
+
className: 'border-0 rounded-none',
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
interface ResponseBodyProps {
|
|
27
|
+
example: string;
|
|
28
|
+
contentType?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Render the example response body: content-type strip + copy button
|
|
32
|
+
* header, then the JSON tree (falling back to a ``<pre>`` if the
|
|
33
|
+
* example isn't valid JSON — sampler occasionally emits non-JSON for
|
|
34
|
+
* exotic content types). */
|
|
35
|
+
export function ResponseBody({ example, contentType }: ResponseBodyProps) {
|
|
36
|
+
// Parse once and cache. ``example`` is pre-stringified by the
|
|
37
|
+
// sampler; JsonTree wants a live object to render nodes with proper
|
|
38
|
+
// folding, so we flip it back here.
|
|
39
|
+
const parsed = useMemo(() => {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(example);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}, [example]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="border-t bg-muted/20">
|
|
49
|
+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border/50">
|
|
50
|
+
<code className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground/70">
|
|
51
|
+
{contentType ?? 'application/json'}
|
|
52
|
+
</code>
|
|
53
|
+
<CopyButton
|
|
54
|
+
value={example}
|
|
55
|
+
variant="ghost"
|
|
56
|
+
size="sm"
|
|
57
|
+
className="h-6 px-2 text-[10px] text-muted-foreground"
|
|
58
|
+
>
|
|
59
|
+
Copy
|
|
60
|
+
</CopyButton>
|
|
61
|
+
</div>
|
|
62
|
+
{parsed != null ? (
|
|
63
|
+
<JsonTree
|
|
64
|
+
title=""
|
|
65
|
+
data={parsed}
|
|
66
|
+
mode="compact"
|
|
67
|
+
config={EXAMPLE_JSON_TREE_CONFIG}
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<pre className="p-3 text-[11px] font-mono text-foreground/70 whitespace-pre-wrap break-all leading-relaxed">
|
|
71
|
+
{example}
|
|
72
|
+
</pre>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronRight } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
9
|
+
import { ResponseBody } from './ResponseBody';
|
|
10
|
+
import { StatusTag } from './StatusTag';
|
|
11
|
+
|
|
12
|
+
interface ResponseRowProps {
|
|
13
|
+
response: NonNullable<ApiEndpoint['responses']>[number];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One status-code row in the Responses section.
|
|
17
|
+
*
|
|
18
|
+
* Layout:
|
|
19
|
+
* - Chevron column only renders when there's an example to expand.
|
|
20
|
+
* Otherwise the row is inert (no button wrapper, no hover state)
|
|
21
|
+
* and the space stays tight instead of reserving an empty gutter
|
|
22
|
+
* under a disabled chevron.
|
|
23
|
+
* - Status tag column is ``48px`` wide — enough for ``default`` and
|
|
24
|
+
* 3-digit codes in the font size we use. Keeps descriptions
|
|
25
|
+
* aligned on their left edge across all rows regardless of code.
|
|
26
|
+
*
|
|
27
|
+
* Expansion defaults:
|
|
28
|
+
* - 2xx with example → open (happy-path is what readers want first).
|
|
29
|
+
* - Everything else → closed, one click to inspect. */
|
|
30
|
+
export function ResponseRow({ response }: ResponseRowProps) {
|
|
31
|
+
const hasExample = Boolean(response.example);
|
|
32
|
+
const numeric = Number.parseInt(response.code, 10);
|
|
33
|
+
const isSuccess = Number.isFinite(numeric) && numeric >= 200 && numeric < 300;
|
|
34
|
+
const [open, setOpen] = useState(hasExample && isSuccess);
|
|
35
|
+
|
|
36
|
+
// Inert row (no example available) — render as a plain div so the
|
|
37
|
+
// layout stays identical but without button affordances.
|
|
38
|
+
if (!hasExample) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex items-center gap-3 px-3 py-2 bg-background">
|
|
41
|
+
<div className="w-12 shrink-0 flex justify-start">
|
|
42
|
+
<StatusTag code={response.code} />
|
|
43
|
+
</div>
|
|
44
|
+
<span className="text-sm text-muted-foreground leading-relaxed break-words min-w-0">
|
|
45
|
+
{response.description}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="bg-background">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => setOpen((v) => !v)}
|
|
56
|
+
className="w-full flex items-center gap-3 px-3 py-2 text-left hover:bg-muted/40 cursor-pointer transition-colors"
|
|
57
|
+
aria-expanded={open}
|
|
58
|
+
>
|
|
59
|
+
<ChevronRight
|
|
60
|
+
className={cn(
|
|
61
|
+
'h-3.5 w-3.5 text-muted-foreground/60 transition-transform shrink-0',
|
|
62
|
+
open && 'rotate-90',
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
<div className="w-12 shrink-0 flex justify-start">
|
|
66
|
+
<StatusTag code={response.code} />
|
|
67
|
+
</div>
|
|
68
|
+
<span className="text-sm text-muted-foreground leading-relaxed break-words min-w-0 flex-1">
|
|
69
|
+
{response.description}
|
|
70
|
+
</span>
|
|
71
|
+
</button>
|
|
72
|
+
{open && (
|
|
73
|
+
<ResponseBody
|
|
74
|
+
example={response.example!}
|
|
75
|
+
contentType={response.contentType}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
interface StatusTagProps {
|
|
6
|
+
code: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** HTTP status pill. Colours map to the usual conventions: 2xx green,
|
|
10
|
+
* 3xx blue, 4xx amber, 5xx red, non-numeric (``default``) neutral.
|
|
11
|
+
* Codes that don't parse as integers render in the neutral style so we
|
|
12
|
+
* never flash a wrong-severity colour at the user. */
|
|
13
|
+
export function StatusTag({ code }: StatusTagProps) {
|
|
14
|
+
const numeric = Number.parseInt(code, 10);
|
|
15
|
+
const cls = !Number.isFinite(numeric)
|
|
16
|
+
? 'bg-muted text-muted-foreground border-border'
|
|
17
|
+
: numeric >= 500
|
|
18
|
+
? 'bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/25'
|
|
19
|
+
: numeric >= 400
|
|
20
|
+
? 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/25'
|
|
21
|
+
: numeric >= 300
|
|
22
|
+
? 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/25'
|
|
23
|
+
: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/25';
|
|
24
|
+
return (
|
|
25
|
+
<span className={cn(
|
|
26
|
+
'inline-flex items-center justify-center rounded border px-2 py-0.5 font-mono text-[11px] font-bold leading-none shrink-0 tabular-nums',
|
|
27
|
+
cls,
|
|
28
|
+
)}>
|
|
29
|
+
{code}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
4
|
+
import { ResponseRow } from './ResponseRow';
|
|
5
|
+
|
|
6
|
+
interface ResponsesProps {
|
|
7
|
+
responses: NonNullable<ApiEndpoint['responses']>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Flat list of response rows. Outer collapsible wrapper lives in the
|
|
11
|
+
* ``Section`` component higher up; this component only knows how to
|
|
12
|
+
* arrange the individual rows. */
|
|
13
|
+
export function Responses({ responses }: ResponsesProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="divide-y border rounded-md overflow-hidden">
|
|
16
|
+
{responses.map((r) => (
|
|
17
|
+
<ResponseRow key={r.code} response={r} />
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|