@djangocfg/ui-tools 2.1.318 → 2.1.319
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/dist/{DocsLayout-ESVQZO3V.mjs → DocsLayout-CTJINVBM.mjs} +235 -267
- package/dist/DocsLayout-CTJINVBM.mjs.map +1 -0
- package/dist/{DocsLayout-KUPDWJ3G.cjs → DocsLayout-XLDB6CJ2.cjs} +273 -305
- package/dist/DocsLayout-XLDB6CJ2.cjs.map +1 -0
- package/dist/{chunk-GBLQTHWT.mjs → chunk-62Y65TGK.mjs} +5 -4
- package/dist/chunk-62Y65TGK.mjs.map +1 -0
- package/dist/{chunk-S44PW6NK.cjs → chunk-TKSFZHCG.cjs} +5 -4
- package/dist/chunk-TKSFZHCG.cjs.map +1 -0
- package/dist/index.cjs +10 -10
- package/dist/index.mjs +4 -4
- package/package.json +6 -6
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +30 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +35 -50
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +49 -22
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +1 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +10 -11
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +25 -5
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +18 -33
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +40 -24
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +8 -14
- package/src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts +1 -4
- package/src/tools/OpenapiViewer/utils/operationToHar.ts +2 -1
- package/src/tools/OpenapiViewer/utils/url.ts +9 -2
- package/dist/DocsLayout-ESVQZO3V.mjs.map +0 -1
- package/dist/DocsLayout-KUPDWJ3G.cjs.map +0 -1
- package/dist/chunk-GBLQTHWT.mjs.map +0 -1
- package/dist/chunk-S44PW6NK.cjs.map +0 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +0 -43
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Check,
|
|
5
5
|
ChevronsDownUp,
|
|
6
6
|
ChevronsUpDown,
|
|
7
|
-
FileCode2,
|
|
8
7
|
Link2,
|
|
9
8
|
} from 'lucide-react';
|
|
10
9
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
@@ -23,7 +22,6 @@ import type { SectionId } from '../types';
|
|
|
23
22
|
|
|
24
23
|
interface MetaActionsProps {
|
|
25
24
|
anchor: string;
|
|
26
|
-
endpointMarkdown: string;
|
|
27
25
|
/** Sections present on this endpoint — expand/collapse acts only
|
|
28
26
|
* on visible rows, never on catalogue items the card doesn't render. */
|
|
29
27
|
presentSections: readonly SectionId[];
|
|
@@ -63,19 +61,22 @@ function IconButton({ label, onClick, children, active }: IconButtonProps) {
|
|
|
63
61
|
);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
/** Inline meta-row actions: copy link ·
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
64
|
+
/** Inline meta-row actions: copy link · expand/collapse all. Actions
|
|
65
|
+
* are always visible (corporate tool pattern) but sized down so the
|
|
66
|
+
* path on the next line stays the visual focal point. The
|
|
67
|
+
* AI-friendly "AI Copy" used to live here as another icon — it was
|
|
68
|
+
* invisible at that size, so it now sits next to "Try it" as a
|
|
69
|
+
* proper labelled button. */
|
|
70
|
+
export function MetaActions({ anchor, presentSections }: MetaActionsProps) {
|
|
70
71
|
const { endpointId } = useEndpointDocContext();
|
|
71
72
|
const expandAll = useEndpointDocStore((s) => s.expandAll);
|
|
72
73
|
const collapseAll = useEndpointDocStore((s) => s.collapseAll);
|
|
73
74
|
const openSections = useEndpointDocStore((s) => s.openSections);
|
|
74
75
|
|
|
75
|
-
const [
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
setTimeout(() =>
|
|
76
|
+
const [linkCopied, setLinkCopied] = useState(false);
|
|
77
|
+
const flashLink = useCallback(() => {
|
|
78
|
+
setLinkCopied(true);
|
|
79
|
+
setTimeout(() => setLinkCopied(false), 1200);
|
|
79
80
|
}, []);
|
|
80
81
|
|
|
81
82
|
const mostlyOpen = useMemo(() => {
|
|
@@ -90,56 +91,40 @@ export function MetaActions({ anchor, endpointMarkdown, presentSections }: MetaA
|
|
|
90
91
|
const copyLink = useCallback(() => {
|
|
91
92
|
if (typeof window === 'undefined') return;
|
|
92
93
|
const url = `${window.location.origin}${window.location.pathname}#${anchor}`;
|
|
93
|
-
void navigator.clipboard?.writeText(url).then(
|
|
94
|
-
}, [anchor,
|
|
95
|
-
|
|
96
|
-
const copyMarkdown = useCallback(() => {
|
|
97
|
-
if (typeof window === 'undefined') return;
|
|
98
|
-
void navigator.clipboard?.writeText(endpointMarkdown).then(() => flash('md'));
|
|
99
|
-
}, [endpointMarkdown, flash]);
|
|
94
|
+
void navigator.clipboard?.writeText(url).then(flashLink);
|
|
95
|
+
}, [anchor, flashLink]);
|
|
100
96
|
|
|
101
97
|
const toggleAll = useCallback(() => {
|
|
102
98
|
if (mostlyOpen) collapseAll(endpointId, presentSections);
|
|
103
99
|
else expandAll(endpointId, presentSections);
|
|
104
100
|
}, [mostlyOpen, collapseAll, expandAll, endpointId, presentSections]);
|
|
105
101
|
|
|
102
|
+
const linkLabel = linkCopied ? 'Copied!' : 'Copy link to endpoint';
|
|
103
|
+
const linkIcon = linkCopied ? (
|
|
104
|
+
<Check className="h-3.5 w-3.5" />
|
|
105
|
+
) : (
|
|
106
|
+
<Link2 className="h-3.5 w-3.5" />
|
|
107
|
+
);
|
|
108
|
+
const showToggleAll = presentSections.length >= 2;
|
|
109
|
+
const toggleAllLabel = mostlyOpen ? 'Collapse all sections' : 'Expand all sections';
|
|
110
|
+
const toggleAllIcon = mostlyOpen ? (
|
|
111
|
+
<ChevronsDownUp className="h-3.5 w-3.5" />
|
|
112
|
+
) : (
|
|
113
|
+
<ChevronsUpDown className="h-3.5 w-3.5" />
|
|
114
|
+
);
|
|
115
|
+
const toggleAllNode = showToggleAll ? (
|
|
116
|
+
<IconButton label={toggleAllLabel} onClick={toggleAll}>
|
|
117
|
+
{toggleAllIcon}
|
|
118
|
+
</IconButton>
|
|
119
|
+
) : null;
|
|
120
|
+
|
|
106
121
|
return (
|
|
107
122
|
<SafeTooltipProvider delayDuration={200}>
|
|
108
123
|
<div className="flex items-center gap-0.5">
|
|
109
|
-
<IconButton
|
|
110
|
-
|
|
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
|
-
)}
|
|
124
|
+
<IconButton label={linkLabel} onClick={copyLink} active={linkCopied}>
|
|
125
|
+
{linkIcon}
|
|
130
126
|
</IconButton>
|
|
131
|
-
{
|
|
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
|
-
)}
|
|
127
|
+
{toggleAllNode}
|
|
143
128
|
</div>
|
|
144
129
|
</SafeTooltipProvider>
|
|
145
130
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Play } from 'lucide-react';
|
|
4
|
-
import React, { useMemo } from 'react';
|
|
3
|
+
import { Check, Play, Sparkles } from 'lucide-react';
|
|
4
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
import { Button } from '@djangocfg/ui-core/components';
|
|
7
7
|
|
|
@@ -43,29 +43,60 @@ export function EndpointHeader({
|
|
|
43
43
|
// reference changes, not on unrelated re-renders of the subtree.
|
|
44
44
|
const endpointMd = useMemo(() => endpointToMarkdown(endpoint), [endpoint]);
|
|
45
45
|
|
|
46
|
+
const [aiCopied, setAiCopied] = useState(false);
|
|
47
|
+
const onAiCopy = useCallback(() => {
|
|
48
|
+
if (typeof window === 'undefined') return;
|
|
49
|
+
void navigator.clipboard?.writeText(endpointMd).then(() => {
|
|
50
|
+
setAiCopied(true);
|
|
51
|
+
setTimeout(() => setAiCopied(false), 1200);
|
|
52
|
+
});
|
|
53
|
+
}, [endpointMd]);
|
|
54
|
+
|
|
55
|
+
const tryItLabel = isLoadedInPlayground ? 'Loaded' : 'Try it';
|
|
56
|
+
const aiCopyIcon = aiCopied ? (
|
|
57
|
+
<Check className="h-3 w-3" />
|
|
58
|
+
) : (
|
|
59
|
+
<Sparkles className="h-3 w-3" />
|
|
60
|
+
);
|
|
61
|
+
const aiCopyLabel = aiCopied ? 'Copied' : 'AI Copy';
|
|
62
|
+
const descriptionNode = endpoint.description ? (
|
|
63
|
+
<div className="text-muted-foreground text-sm">
|
|
64
|
+
<MarkdownMessage content={endpoint.description} />
|
|
65
|
+
</div>
|
|
66
|
+
) : null;
|
|
67
|
+
|
|
46
68
|
return (
|
|
47
69
|
<header className="space-y-3">
|
|
48
70
|
{/* Row 1 — meta strip. Badge + inline icon actions on the
|
|
49
|
-
left, primary
|
|
71
|
+
left, primary CTAs on the right. Kept tight (24px tall)
|
|
50
72
|
so it doesn't visually compete with the path row below. */}
|
|
51
73
|
<div className="flex items-center gap-3 flex-wrap">
|
|
52
74
|
<div className="flex items-center gap-2 min-w-0">
|
|
53
75
|
<MethodBadge method={endpoint.method} />
|
|
54
|
-
<MetaActions
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
<MetaActions anchor={anchor} presentSections={presentSections} />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="ml-auto flex items-center gap-2">
|
|
79
|
+
<Button
|
|
80
|
+
size="sm"
|
|
81
|
+
variant="secondary"
|
|
82
|
+
onClick={onAiCopy}
|
|
83
|
+
title="Copy endpoint as Markdown for AI"
|
|
84
|
+
aria-label="Copy endpoint as Markdown for AI"
|
|
85
|
+
className="h-7 text-xs gap-1.5 px-2.5"
|
|
86
|
+
>
|
|
87
|
+
{aiCopyIcon}
|
|
88
|
+
{aiCopyLabel}
|
|
89
|
+
</Button>
|
|
90
|
+
<Button
|
|
91
|
+
size="sm"
|
|
92
|
+
variant={isLoadedInPlayground ? 'secondary' : 'default'}
|
|
93
|
+
onClick={onTryIt}
|
|
94
|
+
className="h-7 text-xs gap-1.5 px-2.5"
|
|
95
|
+
>
|
|
96
|
+
<Play className="h-3 w-3" />
|
|
97
|
+
{tryItLabel}
|
|
98
|
+
</Button>
|
|
59
99
|
</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
100
|
</div>
|
|
70
101
|
|
|
71
102
|
{/* Row 2 — path as the visual focal point. Larger and more
|
|
@@ -77,11 +108,7 @@ export function EndpointHeader({
|
|
|
77
108
|
|
|
78
109
|
{/* Row 3 — description, aligned to the left edge under the
|
|
79
110
|
path. Text-sm so it reads as subtitle, not body copy. */}
|
|
80
|
-
{
|
|
81
|
-
<div className="text-muted-foreground text-sm">
|
|
82
|
-
<MarkdownMessage content={endpoint.description} />
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
111
|
+
{descriptionNode}
|
|
85
112
|
</header>
|
|
86
113
|
);
|
|
87
114
|
}
|
|
@@ -37,7 +37,7 @@ export function Section({ id, title, badge, children }: SectionProps) {
|
|
|
37
37
|
title={title}
|
|
38
38
|
badge={badge}
|
|
39
39
|
open={open}
|
|
40
|
-
onToggle={() => toggleSection(endpointId, id)}
|
|
40
|
+
onToggle={() => toggleSection(endpointId, id, defaultOpen)}
|
|
41
41
|
/>
|
|
42
42
|
{open && <div>{children}</div>}
|
|
43
43
|
</div>
|
|
@@ -26,7 +26,13 @@ export interface EndpointDocState {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface EndpointDocActions {
|
|
29
|
-
|
|
29
|
+
/** Flip the section's open state. ``defaultOpen`` is the value the
|
|
30
|
+
* ``Section`` component currently shows when the user has no
|
|
31
|
+
* explicit override yet — passing it in lets the first click
|
|
32
|
+
* always invert what's actually on screen, instead of always
|
|
33
|
+
* setting ``false`` (which silently no-ops when the section was
|
|
34
|
+
* closed by default). */
|
|
35
|
+
toggleSection: (endpointId: string, sectionId: SectionId, defaultOpen: boolean) => void;
|
|
30
36
|
setSectionOpen: (endpointId: string, sectionId: SectionId, open: boolean) => void;
|
|
31
37
|
setCodeTab: (endpointId: string, tab: CodeSampleTargetId) => void;
|
|
32
38
|
/** Bulk ops — "expand all" / "collapse all" on a single endpoint.
|
|
@@ -51,22 +57,15 @@ export const useEndpointDocStore = create<EndpointDocStore>()(
|
|
|
51
57
|
(set) => ({
|
|
52
58
|
...initialState,
|
|
53
59
|
|
|
54
|
-
toggleSection: (endpointId, sectionId) =>
|
|
60
|
+
toggleSection: (endpointId, sectionId, defaultOpen) =>
|
|
55
61
|
set((state) => {
|
|
56
62
|
const key = sectionKey(endpointId, sectionId);
|
|
57
63
|
const current = state.openSections[key];
|
|
64
|
+
const visible = current === undefined ? defaultOpen : current;
|
|
58
65
|
return {
|
|
59
66
|
openSections: {
|
|
60
67
|
...state.openSections,
|
|
61
|
-
|
|
62
|
-
// first click means "flip from the default". We
|
|
63
|
-
// assume the default was ``true`` for the most
|
|
64
|
-
// common case (bodies/responses) and ``false``
|
|
65
|
-
// otherwise; the Section component tracks this
|
|
66
|
-
// via its ``defaultOpen`` prop and never calls
|
|
67
|
-
// toggle on sections whose defaults match the
|
|
68
|
-
// next state.
|
|
69
|
-
[key]: current === undefined ? false : !current,
|
|
68
|
+
[key]: !visible,
|
|
70
69
|
},
|
|
71
70
|
};
|
|
72
71
|
}),
|
|
@@ -48,8 +48,14 @@ interface SchemaCopyMenuProps {
|
|
|
48
48
|
/** Trigger appearance.
|
|
49
49
|
* - ``button`` (default) — labelled pill with icon + chevron.
|
|
50
50
|
* - ``icon`` — square ghost button, used in tight spots like the
|
|
51
|
-
* sidebar header where there is no room for "Copy for AI".
|
|
52
|
-
|
|
51
|
+
* sidebar header where there is no room for "Copy for AI".
|
|
52
|
+
* - ``footer`` — full-width secondary CTA, designed to sit at the
|
|
53
|
+
* bottom of the sidebar. Menu opens upward so it stays inside
|
|
54
|
+
* the panel. */
|
|
55
|
+
variant?: 'button' | 'icon' | 'footer';
|
|
56
|
+
/** Where the dropdown content opens. Defaults to ``right`` for the
|
|
57
|
+
* inline triggers; ``footer`` overrides to ``top``. */
|
|
58
|
+
side?: 'right' | 'top' | 'bottom' | 'left';
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -58,7 +64,7 @@ interface SchemaCopyMenuProps {
|
|
|
58
64
|
* dereferencing + stringifying a large schema can be non-trivial — sizes
|
|
59
65
|
* are displayed after the first successful copy, via a tiny cache.
|
|
60
66
|
*/
|
|
61
|
-
export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button' }: SchemaCopyMenuProps) {
|
|
67
|
+
export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button', side }: SchemaCopyMenuProps) {
|
|
62
68
|
const [sizeCache, setSizeCache] = useState<Partial<Record<Flavour, string>>>({});
|
|
63
69
|
const [justCopied, setJustCopied] = useState<Flavour | null>(null);
|
|
64
70
|
const [open, setOpen] = useState(false);
|
|
@@ -98,6 +104,9 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
|
|
|
98
104
|
|
|
99
105
|
const flavours = useMemo<Flavour[]>(() => ['markdown', 'compact', 'raw'], []);
|
|
100
106
|
|
|
107
|
+
const resolvedSide = side ?? (variant === 'footer' ? 'top' : 'right');
|
|
108
|
+
const resolvedAlign = variant === 'footer' ? 'center' : 'start';
|
|
109
|
+
|
|
101
110
|
return (
|
|
102
111
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
103
112
|
<DropdownMenuTrigger asChild>
|
|
@@ -112,6 +121,17 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
|
|
|
112
121
|
>
|
|
113
122
|
<Sparkles className="h-3.5 w-3.5" />
|
|
114
123
|
</Button>
|
|
124
|
+
) : variant === 'footer' ? (
|
|
125
|
+
<Button
|
|
126
|
+
variant="secondary"
|
|
127
|
+
size="sm"
|
|
128
|
+
className="w-full justify-center gap-1.5 text-xs"
|
|
129
|
+
disabled={!isReady}
|
|
130
|
+
>
|
|
131
|
+
<Sparkles className="h-3 w-3" />
|
|
132
|
+
Copy schema for AI
|
|
133
|
+
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
134
|
+
</Button>
|
|
115
135
|
) : (
|
|
116
136
|
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" disabled={!isReady}>
|
|
117
137
|
<Sparkles className="h-3 w-3" />
|
|
@@ -121,8 +141,8 @@ export function SchemaCopyMenu({ schema, endpoints, baseUrl, variant = 'button'
|
|
|
121
141
|
)}
|
|
122
142
|
</DropdownMenuTrigger>
|
|
123
143
|
<DropdownMenuContent
|
|
124
|
-
side=
|
|
125
|
-
align=
|
|
144
|
+
side={resolvedSide}
|
|
145
|
+
align={resolvedAlign}
|
|
126
146
|
sideOffset={6}
|
|
127
147
|
collisionPadding={8}
|
|
128
148
|
className="w-60 max-w-[calc(100vw-16px)]"
|
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import { SchemaCopyMenu } from '../SchemaCopyMenu';
|
|
3
|
+
import type { OpenApiInfo } from '../../../types';
|
|
5
4
|
|
|
6
5
|
interface BrandHeaderProps {
|
|
7
6
|
info: OpenApiInfo | null;
|
|
8
|
-
/** Used only by ``SchemaCopyMenu`` — displayed label comes from ``info``. */
|
|
9
|
-
endpoints: ApiEndpoint[];
|
|
10
|
-
rawSchema?: OpenApiSchema | null;
|
|
11
|
-
resolvedBaseUrl?: string;
|
|
12
7
|
}
|
|
13
8
|
|
|
14
9
|
/** Topmost row of the sidebar: API title on the left, tiny version
|
|
15
|
-
* tag below it
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
export function BrandHeader({ info, endpoints, rawSchema, resolvedBaseUrl }: BrandHeaderProps) {
|
|
10
|
+
* tag below it. The Copy-for-AI menu used to live here as a cramped
|
|
11
|
+
* icon button; it now sits as a full-width secondary CTA in the
|
|
12
|
+
* sidebar footer where it's actually visible. */
|
|
13
|
+
export function BrandHeader({ info }: BrandHeaderProps) {
|
|
20
14
|
const apiTitle = info?.title ?? 'API Reference';
|
|
21
|
-
const
|
|
15
|
+
const versionLabel = info?.version ? `v${info.version}` : null;
|
|
16
|
+
const versionNode = versionLabel ? (
|
|
17
|
+
<div className="font-mono text-[10px] text-muted-foreground/60 leading-tight mt-0.5">
|
|
18
|
+
{versionLabel}
|
|
19
|
+
</div>
|
|
20
|
+
) : null;
|
|
22
21
|
|
|
23
22
|
return (
|
|
24
|
-
<div className="shrink-0 border-b px-3 py-2.5
|
|
25
|
-
<div
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{apiTitle}
|
|
31
|
-
</div>
|
|
32
|
-
{info?.version && (
|
|
33
|
-
<div className="font-mono text-[10px] text-muted-foreground/60 leading-tight mt-0.5">
|
|
34
|
-
v{info.version}
|
|
35
|
-
</div>
|
|
36
|
-
)}
|
|
23
|
+
<div className="shrink-0 border-b px-3 py-2.5">
|
|
24
|
+
<div
|
|
25
|
+
className="text-[13px] font-semibold text-foreground leading-tight truncate"
|
|
26
|
+
title={apiTitle}
|
|
27
|
+
>
|
|
28
|
+
{apiTitle}
|
|
37
29
|
</div>
|
|
38
|
-
{
|
|
39
|
-
<SchemaCopyMenu
|
|
40
|
-
schema={rawSchema ?? null}
|
|
41
|
-
endpoints={endpoints}
|
|
42
|
-
baseUrl={resolvedBaseUrl}
|
|
43
|
-
variant="icon"
|
|
44
|
-
/>
|
|
45
|
-
)}
|
|
30
|
+
{versionNode}
|
|
46
31
|
</div>
|
|
47
32
|
);
|
|
48
33
|
}
|
|
@@ -4,10 +4,9 @@ import React from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { Combobox } from '@djangocfg/ui-core/components';
|
|
6
6
|
|
|
7
|
-
import type { SchemaSource } from '../../../types';
|
|
8
|
-
import {
|
|
7
|
+
import type { ApiEndpoint, OpenApiSchema, SchemaSource } from '../../../types';
|
|
8
|
+
import { SchemaCopyMenu } from '../SchemaCopyMenu';
|
|
9
9
|
import { SearchInput } from './SearchInput';
|
|
10
|
-
import type { MethodFilter } from './types';
|
|
11
10
|
|
|
12
11
|
interface ToolbarProps {
|
|
13
12
|
schemas: SchemaSource[];
|
|
@@ -18,17 +17,21 @@ interface ToolbarProps {
|
|
|
18
17
|
search: string;
|
|
19
18
|
onSearchChange: (v: string) => void;
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/** Active-schema endpoints + raw document — drive the Copy-for-AI
|
|
21
|
+
* CTA that sits under the search input. ``null``/empty disables it. */
|
|
22
|
+
endpoints: ApiEndpoint[];
|
|
23
|
+
rawSchema?: OpenApiSchema | null;
|
|
24
|
+
resolvedBaseUrl?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Filter / control panel of the sidebar. Groups the schema selector,
|
|
26
|
-
* search box, and
|
|
27
|
-
* block so they read as "one toolbar" rather than
|
|
28
|
-
*
|
|
28
|
+
* search box, and the Copy-for-AI CTA into a single visually cohesive
|
|
29
|
+
* block so they read as "one toolbar" rather than separate affordances
|
|
30
|
+
* stacked on top of each other.
|
|
29
31
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* The Copy CTA sits directly under the search input — it's a quiet
|
|
33
|
+
* secondary action, full-width, and previously hid behind a tiny
|
|
34
|
+
* icon in the brand row where users couldn't see it. */
|
|
32
35
|
export function Toolbar({
|
|
33
36
|
schemas,
|
|
34
37
|
currentSchemaId,
|
|
@@ -36,29 +39,42 @@ export function Toolbar({
|
|
|
36
39
|
showSchemaSelector,
|
|
37
40
|
search,
|
|
38
41
|
onSearchChange,
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
endpoints,
|
|
43
|
+
rawSchema,
|
|
44
|
+
resolvedBaseUrl,
|
|
41
45
|
}: ToolbarProps) {
|
|
42
46
|
const schemaOptions = React.useMemo(
|
|
43
47
|
() => schemas.map((s) => ({ value: s.id, label: s.name })),
|
|
44
48
|
[schemas],
|
|
45
49
|
);
|
|
46
50
|
|
|
51
|
+
const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
|
|
52
|
+
const schemaSelectorNode = showSchemaSelector ? (
|
|
53
|
+
<Combobox
|
|
54
|
+
options={schemaOptions}
|
|
55
|
+
value={currentSchemaId ?? ''}
|
|
56
|
+
onValueChange={(id) => id && onSchemaChange(id)}
|
|
57
|
+
placeholder="Select API"
|
|
58
|
+
searchPlaceholder="Search APIs…"
|
|
59
|
+
emptyText="No APIs found"
|
|
60
|
+
className="w-full h-8 text-xs"
|
|
61
|
+
/>
|
|
62
|
+
) : null;
|
|
63
|
+
const copyMenuNode = copyReady ? (
|
|
64
|
+
<SchemaCopyMenu
|
|
65
|
+
schema={rawSchema ?? null}
|
|
66
|
+
endpoints={endpoints}
|
|
67
|
+
baseUrl={resolvedBaseUrl}
|
|
68
|
+
variant="footer"
|
|
69
|
+
side="bottom"
|
|
70
|
+
/>
|
|
71
|
+
) : null;
|
|
72
|
+
|
|
47
73
|
return (
|
|
48
74
|
<div className="shrink-0 border-b px-3 py-2.5 space-y-2">
|
|
49
|
-
{
|
|
50
|
-
<Combobox
|
|
51
|
-
options={schemaOptions}
|
|
52
|
-
value={currentSchemaId ?? ''}
|
|
53
|
-
onValueChange={(id) => id && onSchemaChange(id)}
|
|
54
|
-
placeholder="Select API"
|
|
55
|
-
searchPlaceholder="Search APIs…"
|
|
56
|
-
emptyText="No APIs found"
|
|
57
|
-
className="w-full h-8 text-xs"
|
|
58
|
-
/>
|
|
59
|
-
)}
|
|
75
|
+
{schemaSelectorNode}
|
|
60
76
|
<SearchInput value={search} onChange={onSearchChange} />
|
|
61
|
-
|
|
77
|
+
{copyMenuNode}
|
|
62
78
|
</div>
|
|
63
79
|
);
|
|
64
80
|
}
|
|
@@ -8,7 +8,7 @@ import { BrandHeader } from './BrandHeader';
|
|
|
8
8
|
import { buildFlatVM, buildSectionsVM } from './buildVM';
|
|
9
9
|
import { SidebarBody } from './SidebarBody';
|
|
10
10
|
import { Toolbar } from './Toolbar';
|
|
11
|
-
import type {
|
|
11
|
+
import type { SidebarBodyVM } from './types';
|
|
12
12
|
import { useDebouncedValue } from './useDebouncedValue';
|
|
13
13
|
|
|
14
14
|
export interface DocsSidebarProps {
|
|
@@ -28,7 +28,7 @@ export interface DocsSidebarProps {
|
|
|
28
28
|
* schema id. The sidebar renders them as two-level sections. */
|
|
29
29
|
endpointsBySchema?: Record<string, ApiEndpoint[]>;
|
|
30
30
|
/** Raw active schema + resolved base URL — used by the Copy-for-AI
|
|
31
|
-
*
|
|
31
|
+
* CTA in the toolbar. ``null`` hides the button. */
|
|
32
32
|
rawSchema?: OpenApiSchema | null;
|
|
33
33
|
resolvedBaseUrl?: string;
|
|
34
34
|
}
|
|
@@ -51,7 +51,6 @@ export function DocsSidebar({
|
|
|
51
51
|
resolvedBaseUrl,
|
|
52
52
|
}: DocsSidebarProps) {
|
|
53
53
|
const [search, setSearch] = useState('');
|
|
54
|
-
const [methodFilter, setMethodFilter] = useState<MethodFilter>('ALL');
|
|
55
54
|
const debouncedSearch = useDebouncedValue(search);
|
|
56
55
|
|
|
57
56
|
const body = useMemo<SidebarBodyVM>(() => {
|
|
@@ -61,7 +60,7 @@ export function DocsSidebar({
|
|
|
61
60
|
endpointsBySchema ?? {},
|
|
62
61
|
selectedVersion,
|
|
63
62
|
debouncedSearch,
|
|
64
|
-
|
|
63
|
+
'ALL',
|
|
65
64
|
activeEndpointId,
|
|
66
65
|
);
|
|
67
66
|
}
|
|
@@ -69,7 +68,7 @@ export function DocsSidebar({
|
|
|
69
68
|
endpoints,
|
|
70
69
|
selectedVersion,
|
|
71
70
|
debouncedSearch,
|
|
72
|
-
|
|
71
|
+
'ALL',
|
|
73
72
|
activeEndpointId,
|
|
74
73
|
);
|
|
75
74
|
}, [
|
|
@@ -79,7 +78,6 @@ export function DocsSidebar({
|
|
|
79
78
|
endpoints,
|
|
80
79
|
selectedVersion,
|
|
81
80
|
debouncedSearch,
|
|
82
|
-
methodFilter,
|
|
83
81
|
activeEndpointId,
|
|
84
82
|
]);
|
|
85
83
|
|
|
@@ -88,12 +86,7 @@ export function DocsSidebar({
|
|
|
88
86
|
|
|
89
87
|
return (
|
|
90
88
|
<aside className="flex flex-col h-full min-h-0 border-r bg-muted/10">
|
|
91
|
-
<BrandHeader
|
|
92
|
-
info={info}
|
|
93
|
-
endpoints={endpoints}
|
|
94
|
-
rawSchema={rawSchema}
|
|
95
|
-
resolvedBaseUrl={resolvedBaseUrl}
|
|
96
|
-
/>
|
|
89
|
+
<BrandHeader info={info} />
|
|
97
90
|
<Toolbar
|
|
98
91
|
schemas={schemas}
|
|
99
92
|
currentSchemaId={currentSchemaId}
|
|
@@ -101,8 +94,9 @@ export function DocsSidebar({
|
|
|
101
94
|
showSchemaSelector={showSchemaSelector}
|
|
102
95
|
search={search}
|
|
103
96
|
onSearchChange={setSearch}
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
endpoints={endpoints}
|
|
98
|
+
rawSchema={rawSchema}
|
|
99
|
+
resolvedBaseUrl={resolvedBaseUrl}
|
|
106
100
|
/>
|
|
107
101
|
<ScrollArea>
|
|
108
102
|
<SidebarBody body={body} onNavigate={onNavigate} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiEndpoint } from '../../types';
|
|
2
|
+
import { relativePath } from '../../utils/url';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Given a list of full endpoint paths, return the longest ``/``-aligned
|
|
@@ -50,10 +51,6 @@ export function sidebarLabel(ep: ApiEndpoint, groupCommonPrefix: string): string
|
|
|
50
51
|
return relativePath(ep.path);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function relativePath(full: string): string {
|
|
54
|
-
try { return new URL(full).pathname; } catch { return full; }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
/** Tooltip text: always the definitive ``METHOD relative/path``. */
|
|
58
55
|
export function sidebarTooltip(ep: ApiEndpoint): string {
|
|
59
56
|
return `${ep.method} ${relativePath(ep.path)}`;
|
|
@@ -64,7 +64,8 @@ function buildUrl(
|
|
|
64
64
|
|
|
65
65
|
let path = endpoint.path;
|
|
66
66
|
for (const name of pathParamNames) {
|
|
67
|
-
const value = parameters[name]
|
|
67
|
+
const value = parameters[name];
|
|
68
|
+
if (value === undefined || value === '') continue;
|
|
68
69
|
path = path.replaceAll(`{${name}}`, encodeURIComponent(value));
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -118,9 +118,16 @@ export function resolveAbsolute(url: string): string {
|
|
|
118
118
|
return url;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** Pull just the path out of any URL (absolute or relative).
|
|
121
|
+
/** Pull just the path out of any URL (absolute or relative). The
|
|
122
|
+
* ``URL`` parser percent-encodes path segments, which turns OpenAPI
|
|
123
|
+
* template braces (``{id}``) into ``%7Bid%7D``. We restore those so
|
|
124
|
+
* unfilled templates render as the readable form authors wrote. */
|
|
122
125
|
export function relativePath(url: string): string {
|
|
123
|
-
try {
|
|
126
|
+
try {
|
|
127
|
+
return new URL(url).pathname.replace(/%7B/gi, '{').replace(/%7D/gi, '}');
|
|
128
|
+
} catch {
|
|
129
|
+
return url;
|
|
130
|
+
}
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
/**
|