@djangocfg/ui-tools 2.1.289 → 2.1.291
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 +18 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.mjs +13 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -15
- package/src/components/markdown/MarkdownMessage.tsx +46 -0
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
- 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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { EndpointRow } from './EndpointRow';
|
|
6
|
+
import type { CategoryVM, NavigateFn } from './types';
|
|
7
|
+
|
|
8
|
+
interface CategoryBlockProps {
|
|
9
|
+
category: CategoryVM;
|
|
10
|
+
onNavigate: NavigateFn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A labelled group of endpoint rows. The category header is
|
|
14
|
+
* deliberately tiny (text-[10px] tracking-wider) — it's a divider, not
|
|
15
|
+
* a call to attention. Users who are reading the list want to see the
|
|
16
|
+
* endpoint names, not the category prose. */
|
|
17
|
+
export const CategoryBlock = React.memo(function CategoryBlock({
|
|
18
|
+
category,
|
|
19
|
+
onNavigate,
|
|
20
|
+
}: CategoryBlockProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="mb-2.5 last:mb-1">
|
|
23
|
+
<div className="px-3 pt-3 pb-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none">
|
|
24
|
+
{category.category}
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
{category.rows.map((row) => (
|
|
28
|
+
<EndpointRow key={row.key} row={row} onNavigate={onNavigate} />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@djangocfg/ui-core/components';
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
import { MethodBadge } from '../../shared/ui';
|
|
9
|
+
import type { EndpointRowVM, NavigateFn } from './types';
|
|
10
|
+
|
|
11
|
+
interface EndpointRowProps {
|
|
12
|
+
row: EndpointRowVM;
|
|
13
|
+
onNavigate: NavigateFn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One endpoint in the sidebar list. Layout decisions worth calling out:
|
|
17
|
+
* - Fixed 52px badge column via CSS grid so ``GET`` / ``POST`` /
|
|
18
|
+
* ``PATCH`` all take the same horizontal slot and labels align
|
|
19
|
+
* vertically on the left edge. Without this the list looks ragged
|
|
20
|
+
* because ``POST`` is wider than ``GET`` and each label starts at
|
|
21
|
+
* a different x offset.
|
|
22
|
+
* - ``items-baseline`` so the method badge sits on the same visual
|
|
23
|
+
* line as the label text, not in its vertical centre.
|
|
24
|
+
* - ``py-1`` (not the old ``py-1.5``) for higher list density.
|
|
25
|
+
* - Trailing full-stops are trimmed from the label. OpenAPI
|
|
26
|
+
* summaries often end in ``.`` which looks like noise in a list.
|
|
27
|
+
* - Active state is a single left-edge accent bar + soft tint — no
|
|
28
|
+
* big filled background so the method badge still carries the
|
|
29
|
+
* colour semantics of the row. */
|
|
30
|
+
export const EndpointRow = React.memo(function EndpointRow({
|
|
31
|
+
row,
|
|
32
|
+
onNavigate,
|
|
33
|
+
}: EndpointRowProps) {
|
|
34
|
+
// Strip a trailing full-stop — OpenAPI ``summary`` routinely ends
|
|
35
|
+
// in ``.`` which looks like punctuation noise when stacked in a list.
|
|
36
|
+
const displayLabel = row.label.replace(/\.$/, '');
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Tooltip delayDuration={350}>
|
|
40
|
+
<TooltipTrigger asChild>
|
|
41
|
+
<button
|
|
42
|
+
onClick={() => onNavigate(row.anchor, row.schemaId)}
|
|
43
|
+
aria-current={row.isActive ? 'location' : undefined}
|
|
44
|
+
className={cn(
|
|
45
|
+
'relative w-full text-left grid grid-cols-[52px_minmax(0,1fr)] items-baseline gap-2 pl-3 pr-3 py-1 transition-colors',
|
|
46
|
+
row.isActive
|
|
47
|
+
? 'bg-primary/10 text-foreground'
|
|
48
|
+
: 'hover:bg-muted/40 text-foreground/75 hover:text-foreground',
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{row.isActive && (
|
|
52
|
+
<span className="absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" />
|
|
53
|
+
)}
|
|
54
|
+
<span className="justify-self-start">
|
|
55
|
+
<MethodBadge method={row.method} />
|
|
56
|
+
</span>
|
|
57
|
+
<span
|
|
58
|
+
className={cn(
|
|
59
|
+
'line-clamp-2 leading-snug min-w-0',
|
|
60
|
+
row.useMono ? 'font-mono text-[11px] break-all' : 'text-[12px]',
|
|
61
|
+
row.isActive && 'text-foreground font-medium',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{displayLabel}
|
|
65
|
+
</span>
|
|
66
|
+
</button>
|
|
67
|
+
</TooltipTrigger>
|
|
68
|
+
<TooltipContent side="right" align="center" className="font-mono text-[11px]">
|
|
69
|
+
{row.tooltip}
|
|
70
|
+
</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { METHOD_FILTERS, type MethodFilter } from './types';
|
|
6
|
+
|
|
7
|
+
interface MethodChipsProps {
|
|
8
|
+
value: MethodFilter;
|
|
9
|
+
onChange: (v: MethodFilter) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Horizontal filter strip. Scrolls horizontally on narrow widths
|
|
13
|
+
* rather than wrapping to a second line — the toolbar stays a fixed
|
|
14
|
+
* height so the endpoint list below gets every pixel it can.
|
|
15
|
+
*
|
|
16
|
+
* Active chips use a solid fill (not the outlined-with-tint style
|
|
17
|
+
* from the old design) so "this filter is on" reads at a glance
|
|
18
|
+
* without the eye having to resolve subtle border colour shifts. */
|
|
19
|
+
export function MethodChips({ value, onChange }: MethodChipsProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center gap-1 overflow-x-auto -mx-1 px-1 pb-px">
|
|
22
|
+
{METHOD_FILTERS.map((m) => {
|
|
23
|
+
const active = value === m;
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
key={m}
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => onChange(m)}
|
|
29
|
+
aria-pressed={active}
|
|
30
|
+
className={cn(
|
|
31
|
+
'shrink-0 px-2 h-6 rounded font-mono text-[10px] font-semibold tracking-wide uppercase transition-colors',
|
|
32
|
+
active
|
|
33
|
+
? 'bg-foreground text-background'
|
|
34
|
+
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted',
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{m}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CategoryBlock } from './CategoryBlock';
|
|
4
|
+
import type { NavigateFn, SchemaSectionVM } from './types';
|
|
5
|
+
|
|
6
|
+
interface SchemaSectionProps {
|
|
7
|
+
section: SchemaSectionVM;
|
|
8
|
+
onNavigate: NavigateFn;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One schema block inside ``sections`` grouping mode. Renders a
|
|
12
|
+
* sticky header with the schema name so users can tell which API
|
|
13
|
+
* they're looking at while scrolling, then the categories below. */
|
|
14
|
+
export function SchemaSection({ section, onNavigate }: SchemaSectionProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="mb-4 last:mb-2">
|
|
17
|
+
<div className="px-3 py-1.5 sticky top-0 z-[1] bg-background/95 backdrop-blur-[2px] border-b border-border/40">
|
|
18
|
+
<span className="text-[11px] font-bold uppercase tracking-[0.12em] text-foreground/80">
|
|
19
|
+
{section.sourceName}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
{section.categories.map((cat) => (
|
|
23
|
+
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Input } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
interface SearchInputProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (v: string) => void;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Search field with a leading icon and a trailing clear button. The
|
|
16
|
+
* clear button only appears when there's something to clear so the
|
|
17
|
+
* idle state reads as a single clean input rather than "icon + input
|
|
18
|
+
* + mystery button". */
|
|
19
|
+
export function SearchInput({ value, onChange, placeholder }: SearchInputProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="relative">
|
|
22
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" />
|
|
23
|
+
<Input
|
|
24
|
+
placeholder={placeholder ?? 'Search endpoints…'}
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
|
27
|
+
className="pl-8 pr-7 h-8 text-xs"
|
|
28
|
+
/>
|
|
29
|
+
{value && (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => onChange('')}
|
|
33
|
+
aria-label="Clear search"
|
|
34
|
+
className={cn(
|
|
35
|
+
'absolute right-1.5 top-1/2 -translate-y-1/2 h-5 w-5 rounded',
|
|
36
|
+
'inline-flex items-center justify-center',
|
|
37
|
+
'text-muted-foreground/50 hover:text-foreground hover:bg-muted transition-colors',
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<X className="h-3 w-3" />
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CategoryBlock } from './CategoryBlock';
|
|
4
|
+
import { SchemaSection } from './SchemaSection';
|
|
5
|
+
import type { NavigateFn, SidebarBodyVM } from './types';
|
|
6
|
+
|
|
7
|
+
interface SidebarBodyProps {
|
|
8
|
+
body: SidebarBodyVM;
|
|
9
|
+
onNavigate: NavigateFn;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Scrollable body of the sidebar. Picks the rendering strategy based
|
|
13
|
+
* on ``body.kind`` — flat category list for single-schema mode,
|
|
14
|
+
* nested schema sections for multi-schema mode — and falls back to
|
|
15
|
+
* a centred empty-state message when the filter pipeline yields no
|
|
16
|
+
* rows. */
|
|
17
|
+
export function SidebarBody({ body, onNavigate }: SidebarBodyProps) {
|
|
18
|
+
if (body.kind === 'flat') {
|
|
19
|
+
if (body.categories.length === 0) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
22
|
+
{body.emptyText}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<nav className="py-1.5">
|
|
28
|
+
{body.categories.map((cat) => (
|
|
29
|
+
<CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (body.sections.length === 0) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="py-10 px-4 text-center text-xs text-muted-foreground">
|
|
38
|
+
{body.emptyText}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<nav className="py-1.5">
|
|
45
|
+
{body.sections.map((section) => (
|
|
46
|
+
<SchemaSection key={section.sourceId} section={section} onNavigate={onNavigate} />
|
|
47
|
+
))}
|
|
48
|
+
</nav>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { Combobox } from '@djangocfg/ui-core/components';
|
|
6
|
+
|
|
7
|
+
import type { SchemaSource } from '../../../types';
|
|
8
|
+
import { MethodChips } from './MethodChips';
|
|
9
|
+
import { SearchInput } from './SearchInput';
|
|
10
|
+
import type { MethodFilter } from './types';
|
|
11
|
+
|
|
12
|
+
interface ToolbarProps {
|
|
13
|
+
schemas: SchemaSource[];
|
|
14
|
+
currentSchemaId: string | null;
|
|
15
|
+
onSchemaChange: (id: string) => void;
|
|
16
|
+
showSchemaSelector: boolean;
|
|
17
|
+
|
|
18
|
+
search: string;
|
|
19
|
+
onSearchChange: (v: string) => void;
|
|
20
|
+
|
|
21
|
+
methodFilter: MethodFilter;
|
|
22
|
+
onMethodFilterChange: (v: MethodFilter) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Filter / control panel of the sidebar. Groups the schema selector,
|
|
26
|
+
* search box, and HTTP method chips into a single visually cohesive
|
|
27
|
+
* block so they read as "one toolbar" rather than three separate
|
|
28
|
+
* affordances stacked on top of each other.
|
|
29
|
+
*
|
|
30
|
+
* Each row is optional in the sense that we only render the schema
|
|
31
|
+
* selector when there are multiple schemas to choose between. */
|
|
32
|
+
export function Toolbar({
|
|
33
|
+
schemas,
|
|
34
|
+
currentSchemaId,
|
|
35
|
+
onSchemaChange,
|
|
36
|
+
showSchemaSelector,
|
|
37
|
+
search,
|
|
38
|
+
onSearchChange,
|
|
39
|
+
methodFilter,
|
|
40
|
+
onMethodFilterChange,
|
|
41
|
+
}: ToolbarProps) {
|
|
42
|
+
const schemaOptions = React.useMemo(
|
|
43
|
+
() => schemas.map((s) => ({ value: s.id, label: s.name })),
|
|
44
|
+
[schemas],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="shrink-0 border-b px-3 py-2.5 space-y-2">
|
|
49
|
+
{showSchemaSelector && (
|
|
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
|
+
)}
|
|
60
|
+
<SearchInput value={search} onChange={onSearchChange} />
|
|
61
|
+
<MethodChips value={methodFilter} onChange={onMethodFilterChange} />
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ApiEndpoint, OpenApiSchema, SchemaSource } from '../../../types';
|
|
2
|
+
import { deduplicateEndpoints } from '../../../utils/versionManager';
|
|
3
|
+
import { endpointAnchor } from '../anchor';
|
|
4
|
+
import { buildSchemaSections, groupEndpoints, type EndpointGroup } from '../grouping';
|
|
5
|
+
import { sidebarLabel, sidebarTooltip } from '../sidebarLabel';
|
|
6
|
+
import type {
|
|
7
|
+
CategoryVM,
|
|
8
|
+
EndpointRowVM,
|
|
9
|
+
MethodFilter,
|
|
10
|
+
SchemaSectionVM,
|
|
11
|
+
SidebarBodyVM,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
// ``OpenApiSchema`` is imported to satisfy the build VM signatures even
|
|
15
|
+
// though we never read it here — re-exported so consumers can reuse
|
|
16
|
+
// the same type surface without another import.
|
|
17
|
+
export type { OpenApiSchema };
|
|
18
|
+
|
|
19
|
+
export function filterEndpoints(
|
|
20
|
+
list: ApiEndpoint[],
|
|
21
|
+
query: string,
|
|
22
|
+
method: MethodFilter,
|
|
23
|
+
): ApiEndpoint[] {
|
|
24
|
+
let out = list;
|
|
25
|
+
if (method !== 'ALL') {
|
|
26
|
+
out = out.filter((e) => e.method === method);
|
|
27
|
+
}
|
|
28
|
+
if (query) {
|
|
29
|
+
const q = query.toLowerCase();
|
|
30
|
+
out = out.filter(
|
|
31
|
+
(e) =>
|
|
32
|
+
e.summary.toLowerCase().includes(q) ||
|
|
33
|
+
e.name.toLowerCase().includes(q) ||
|
|
34
|
+
e.description.toLowerCase().includes(q) ||
|
|
35
|
+
e.path.toLowerCase().includes(q),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildCategory(
|
|
42
|
+
group: EndpointGroup,
|
|
43
|
+
activeEndpointId: string | null,
|
|
44
|
+
schemaId: string | null,
|
|
45
|
+
keyPrefix: string,
|
|
46
|
+
): CategoryVM {
|
|
47
|
+
const rows: EndpointRowVM[] = group.endpoints.map((ep) => {
|
|
48
|
+
const anchor = endpointAnchor(ep, schemaId ?? ep.schemaId ?? null);
|
|
49
|
+
return {
|
|
50
|
+
key: `${ep.method}-${ep.path}`,
|
|
51
|
+
anchor,
|
|
52
|
+
schemaId: schemaId ?? ep.schemaId ?? null,
|
|
53
|
+
label: sidebarLabel(ep, group.commonPrefix),
|
|
54
|
+
tooltip: sidebarTooltip(ep),
|
|
55
|
+
method: ep.method,
|
|
56
|
+
useMono: !ep.summary,
|
|
57
|
+
isActive: activeEndpointId === anchor,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
key: `${keyPrefix}${group.category}`,
|
|
62
|
+
category: group.category,
|
|
63
|
+
rows,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function emptyTextFor(query: string, method: MethodFilter, defaultText: string): string {
|
|
68
|
+
if (query && method !== 'ALL') return `No ${method} endpoints match "${query}"`;
|
|
69
|
+
if (query) return `No endpoints match "${query}"`;
|
|
70
|
+
if (method !== 'ALL') return `No ${method} endpoints`;
|
|
71
|
+
return defaultText;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildFlatVM(
|
|
75
|
+
endpoints: ApiEndpoint[],
|
|
76
|
+
selectedVersion: string,
|
|
77
|
+
query: string,
|
|
78
|
+
method: MethodFilter,
|
|
79
|
+
activeEndpointId: string | null,
|
|
80
|
+
): SidebarBodyVM {
|
|
81
|
+
const filtered = filterEndpoints(
|
|
82
|
+
deduplicateEndpoints(endpoints, selectedVersion),
|
|
83
|
+
query,
|
|
84
|
+
method,
|
|
85
|
+
);
|
|
86
|
+
const groups = groupEndpoints(filtered);
|
|
87
|
+
return {
|
|
88
|
+
kind: 'flat',
|
|
89
|
+
categories: groups.map((g) => buildCategory(g, activeEndpointId, null, '')),
|
|
90
|
+
emptyText: emptyTextFor(query, method, 'No endpoints in this schema'),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildSectionsVM(
|
|
95
|
+
schemas: SchemaSource[],
|
|
96
|
+
endpointsBySchema: Record<string, ApiEndpoint[]>,
|
|
97
|
+
selectedVersion: string,
|
|
98
|
+
query: string,
|
|
99
|
+
method: MethodFilter,
|
|
100
|
+
activeEndpointId: string | null,
|
|
101
|
+
): SidebarBodyVM {
|
|
102
|
+
const filteredMap: Record<string, ApiEndpoint[]> = {};
|
|
103
|
+
for (const src of schemas) {
|
|
104
|
+
const raw = endpointsBySchema[src.id] ?? [];
|
|
105
|
+
filteredMap[src.id] = filterEndpoints(
|
|
106
|
+
deduplicateEndpoints(raw, selectedVersion),
|
|
107
|
+
query,
|
|
108
|
+
method,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const rawSections = buildSchemaSections(schemas, filteredMap);
|
|
112
|
+
const sections: SchemaSectionVM[] = rawSections
|
|
113
|
+
.filter((s) => s.groups.length > 0)
|
|
114
|
+
.map((s) => ({
|
|
115
|
+
sourceId: s.source.id,
|
|
116
|
+
sourceName: s.source.name,
|
|
117
|
+
categories: s.groups.map((g) =>
|
|
118
|
+
buildCategory(g, activeEndpointId, s.source.id, `${s.source.id}-`),
|
|
119
|
+
),
|
|
120
|
+
}));
|
|
121
|
+
return {
|
|
122
|
+
kind: 'sections',
|
|
123
|
+
sections,
|
|
124
|
+
emptyText: emptyTextFor(query, method, 'No endpoints in any schema'),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema, SchemaSource } from '../../../types';
|
|
6
|
+
import { ScrollArea } from '../../shared/ui';
|
|
7
|
+
import { BrandHeader } from './BrandHeader';
|
|
8
|
+
import { buildFlatVM, buildSectionsVM } from './buildVM';
|
|
9
|
+
import { SidebarBody } from './SidebarBody';
|
|
10
|
+
import { Toolbar } from './Toolbar';
|
|
11
|
+
import type { MethodFilter, SidebarBodyVM } from './types';
|
|
12
|
+
import { useDebouncedValue } from './useDebouncedValue';
|
|
13
|
+
|
|
14
|
+
export interface DocsSidebarProps {
|
|
15
|
+
info: OpenApiInfo | null;
|
|
16
|
+
/** Active-schema endpoints — used by ``selector`` mode. */
|
|
17
|
+
endpoints: ApiEndpoint[];
|
|
18
|
+
/** All configured schemas (used by both modes). */
|
|
19
|
+
schemas: SchemaSource[];
|
|
20
|
+
currentSchemaId: string | null;
|
|
21
|
+
onSchemaChange: (id: string) => void;
|
|
22
|
+
activeEndpointId: string | null;
|
|
23
|
+
selectedVersion: string;
|
|
24
|
+
onNavigate: (anchor: string, schemaId?: string | null) => void;
|
|
25
|
+
/** Presentation mode. Default: ``selector`` (back-compat). */
|
|
26
|
+
grouping?: 'selector' | 'sections';
|
|
27
|
+
/** Required for ``sections`` mode — endpoints grouped by their source
|
|
28
|
+
* schema id. The sidebar renders them as two-level sections. */
|
|
29
|
+
endpointsBySchema?: Record<string, ApiEndpoint[]>;
|
|
30
|
+
/** Raw active schema + resolved base URL — used by the Copy-for-AI
|
|
31
|
+
* dropdown in the brand row. ``null`` disables the button. */
|
|
32
|
+
rawSchema?: OpenApiSchema | null;
|
|
33
|
+
resolvedBaseUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Docs sidebar orchestrator. Thin — each child folder owns its own
|
|
37
|
+
* rendering; this file wires props, debounces search, and decides
|
|
38
|
+
* which view-model builder to call. */
|
|
39
|
+
export function DocsSidebar({
|
|
40
|
+
info,
|
|
41
|
+
endpoints,
|
|
42
|
+
schemas,
|
|
43
|
+
currentSchemaId,
|
|
44
|
+
onSchemaChange,
|
|
45
|
+
activeEndpointId,
|
|
46
|
+
selectedVersion,
|
|
47
|
+
onNavigate,
|
|
48
|
+
grouping = 'selector',
|
|
49
|
+
endpointsBySchema,
|
|
50
|
+
rawSchema,
|
|
51
|
+
resolvedBaseUrl,
|
|
52
|
+
}: DocsSidebarProps) {
|
|
53
|
+
const [search, setSearch] = useState('');
|
|
54
|
+
const [methodFilter, setMethodFilter] = useState<MethodFilter>('ALL');
|
|
55
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
56
|
+
|
|
57
|
+
const body = useMemo<SidebarBodyVM>(() => {
|
|
58
|
+
if (grouping === 'sections') {
|
|
59
|
+
return buildSectionsVM(
|
|
60
|
+
schemas,
|
|
61
|
+
endpointsBySchema ?? {},
|
|
62
|
+
selectedVersion,
|
|
63
|
+
debouncedSearch,
|
|
64
|
+
methodFilter,
|
|
65
|
+
activeEndpointId,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return buildFlatVM(
|
|
69
|
+
endpoints,
|
|
70
|
+
selectedVersion,
|
|
71
|
+
debouncedSearch,
|
|
72
|
+
methodFilter,
|
|
73
|
+
activeEndpointId,
|
|
74
|
+
);
|
|
75
|
+
}, [
|
|
76
|
+
grouping,
|
|
77
|
+
schemas,
|
|
78
|
+
endpointsBySchema,
|
|
79
|
+
endpoints,
|
|
80
|
+
selectedVersion,
|
|
81
|
+
debouncedSearch,
|
|
82
|
+
methodFilter,
|
|
83
|
+
activeEndpointId,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const hasMultipleSchemas = schemas.length > 1;
|
|
87
|
+
const showSchemaSelector = grouping === 'selector' && hasMultipleSchemas;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<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
|
+
/>
|
|
97
|
+
<Toolbar
|
|
98
|
+
schemas={schemas}
|
|
99
|
+
currentSchemaId={currentSchemaId}
|
|
100
|
+
onSchemaChange={onSchemaChange}
|
|
101
|
+
showSchemaSelector={showSchemaSelector}
|
|
102
|
+
search={search}
|
|
103
|
+
onSearchChange={setSearch}
|
|
104
|
+
methodFilter={methodFilter}
|
|
105
|
+
onMethodFilterChange={setMethodFilter}
|
|
106
|
+
/>
|
|
107
|
+
<ScrollArea>
|
|
108
|
+
<SidebarBody body={body} onNavigate={onNavigate} />
|
|
109
|
+
</ScrollArea>
|
|
110
|
+
</aside>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ApiEndpoint } from '../../../types';
|
|
2
|
+
|
|
3
|
+
/** HTTP method filter. ``ALL`` is a sentinel for no filter — using a
|
|
4
|
+
* string union (rather than ``string | null``) keeps the filter chips
|
|
5
|
+
* and reducer logic type-safe without null juggling. */
|
|
6
|
+
export const METHOD_FILTERS = ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
7
|
+
export type MethodFilter = (typeof METHOD_FILTERS)[number];
|
|
8
|
+
|
|
9
|
+
export interface EndpointRowVM {
|
|
10
|
+
key: string;
|
|
11
|
+
anchor: string;
|
|
12
|
+
schemaId: string | null;
|
|
13
|
+
label: string;
|
|
14
|
+
tooltip: string;
|
|
15
|
+
method: string;
|
|
16
|
+
/** Summary-less endpoints get a monospace font for the path-tail
|
|
17
|
+
* label so the tech string is readable against a sea of prose. */
|
|
18
|
+
useMono: boolean;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CategoryVM {
|
|
23
|
+
key: string;
|
|
24
|
+
category: string;
|
|
25
|
+
rows: EndpointRowVM[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SchemaSectionVM {
|
|
29
|
+
sourceId: string;
|
|
30
|
+
sourceName: string;
|
|
31
|
+
categories: CategoryVM[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SidebarBodyVM =
|
|
35
|
+
| { kind: 'flat'; categories: CategoryVM[]; emptyText: string }
|
|
36
|
+
| { kind: 'sections'; sections: SchemaSectionVM[]; emptyText: string };
|
|
37
|
+
|
|
38
|
+
export type NavigateFn = (anchor: string, schemaId?: string | null) => void;
|
|
39
|
+
|
|
40
|
+
// Re-exported here so downstream files don't all re-import from the
|
|
41
|
+
// outer ``types`` module for every component prop.
|
|
42
|
+
export type { ApiEndpoint };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Debounce a fast-changing value. Used for the sidebar search box so
|
|
4
|
+
* each keystroke doesn't re-run the filter pipeline (endpoint list
|
|
5
|
+
* can be hundreds of items long in some specs). 120ms is short
|
|
6
|
+
* enough to feel instant while still batching rapid typing. */
|
|
7
|
+
export function useDebouncedValue<T>(value: T, delayMs = 120): T {
|
|
8
|
+
const [debounced, setDebounced] = useState(value);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
11
|
+
return () => clearTimeout(id);
|
|
12
|
+
}, [value, delayMs]);
|
|
13
|
+
return debounced;
|
|
14
|
+
}
|
|
@@ -71,7 +71,11 @@ export function SlideInPlayground({ open, onClose }: SlideInPlaygroundProps) {
|
|
|
71
71
|
|
|
72
72
|
{/* Body: 1 or 2 columns depending on whether we have a
|
|
73
73
|
response to show. ``divide-x`` gives a 1px seam
|
|
74
|
-
between the panes so they read as distinct surfaces.
|
|
74
|
+
between the panes so they read as distinct surfaces.
|
|
75
|
+
Send button lives inside the Request column so it
|
|
76
|
+
reads as an action on the form it belongs to — not
|
|
77
|
+
as a page-level footer that ambiguously sat under
|
|
78
|
+
the response column too. */}
|
|
75
79
|
<SidePanel.Body
|
|
76
80
|
className={cn(
|
|
77
81
|
'overflow-hidden grid divide-x transition-[grid-template-columns] duration-250',
|
|
@@ -82,6 +86,11 @@ export function SlideInPlayground({ open, onClose }: SlideInPlaygroundProps) {
|
|
|
82
86
|
>
|
|
83
87
|
<Panel>
|
|
84
88
|
<RequestPanel />
|
|
89
|
+
{ep && (
|
|
90
|
+
<div className="shrink-0 border-t px-4 py-3 bg-background">
|
|
91
|
+
<SendButton />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
85
94
|
</Panel>
|
|
86
95
|
{showResponse && (
|
|
87
96
|
<Panel>
|
|
@@ -89,12 +98,6 @@ export function SlideInPlayground({ open, onClose }: SlideInPlaygroundProps) {
|
|
|
89
98
|
</Panel>
|
|
90
99
|
)}
|
|
91
100
|
</SidePanel.Body>
|
|
92
|
-
|
|
93
|
-
{ep && (
|
|
94
|
-
<SidePanel.Footer className="px-4 py-3">
|
|
95
|
-
<SendButton />
|
|
96
|
-
</SidePanel.Footer>
|
|
97
|
-
)}
|
|
98
101
|
</SidePanel.Content>
|
|
99
102
|
</SidePanel>
|
|
100
103
|
);
|