@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
|
@@ -101,6 +101,9 @@ OpenapiViewer/
|
|
|
101
101
|
├── utils/
|
|
102
102
|
│ ├── url.ts # UrlBuilder + path/query/baseUrl helpers
|
|
103
103
|
│ ├── schemaExport.ts # Dereferenced/compact/markdown export for LLMs
|
|
104
|
+
│ ├── sampler.ts # Schema → example value (openapi-sampler wrap)
|
|
105
|
+
│ ├── operationToHar.ts # ApiEndpoint → HAR request shape
|
|
106
|
+
│ ├── codeSamples.ts # HAR → curl / fetch / python / … renderers
|
|
104
107
|
│ ├── formatters.ts # HTTP status / method helpers, JSON validation
|
|
105
108
|
│ ├── versionManager.ts
|
|
106
109
|
│ ├── apiKeyManager.ts
|
|
@@ -110,28 +113,126 @@ OpenapiViewer/
|
|
|
110
113
|
├── index.ts # Re-exports DocsLayout
|
|
111
114
|
├── DocsLayout/ # Default (only) layout
|
|
112
115
|
│ ├── index.tsx # Root: sidebar + docs + slide-in playground
|
|
113
|
-
│ ├── Sidebar.tsx # Grouped endpoint list, scrollspy highlight
|
|
114
116
|
│ ├── DocsView.tsx # Longread scroll container + scrollspy
|
|
115
|
-
│ ├── EndpointDoc.tsx # One endpoint as a prose section + fields table
|
|
116
117
|
│ ├── ApiIntroSection.tsx # Top intro + Copy-for-AI dropdown
|
|
117
118
|
│ ├── SchemaCopyMenu.tsx # Per-schema copy flavours
|
|
118
119
|
│ ├── SlideInPlayground.tsx# Right slide-in overlay
|
|
119
120
|
│ ├── TryItSheet.tsx # Mobile/tablet fallback via ResponsiveSheet
|
|
120
121
|
│ ├── anchor.ts # Stable section anchors for deep-linking
|
|
121
122
|
│ ├── grouping.ts # Shared group+sort used by sidebar and docs
|
|
122
|
-
│ ├── schemaFields.ts # JSON Schema → flat field rows
|
|
123
|
-
│
|
|
123
|
+
│ ├── schemaFields.ts # JSON Schema → flat field rows (legacy helper)
|
|
124
|
+
│ ├── sidebarLabel.ts # Common-prefix strip + tooltip helpers
|
|
125
|
+
│ │
|
|
126
|
+
│ ├── Sidebar/ # Grouped endpoint list + toolbar
|
|
127
|
+
│ │ ├── index.tsx # Orchestrator
|
|
128
|
+
│ │ ├── types.ts # VM types + METHOD_FILTERS
|
|
129
|
+
│ │ ├── buildVM.ts # Pure view-model builders (flat + sections)
|
|
130
|
+
│ │ ├── useDebouncedValue.ts
|
|
131
|
+
│ │ ├── BrandHeader.tsx # Title + version + Copy-for-AI
|
|
132
|
+
│ │ ├── Toolbar.tsx # Schema combobox + search + method chips
|
|
133
|
+
│ │ ├── SearchInput.tsx
|
|
134
|
+
│ │ ├── MethodChips.tsx
|
|
135
|
+
│ │ ├── SidebarBody.tsx # flat vs sections switch
|
|
136
|
+
│ │ ├── SchemaSection.tsx
|
|
137
|
+
│ │ ├── CategoryBlock.tsx
|
|
138
|
+
│ │ └── EndpointRow.tsx
|
|
139
|
+
│ │
|
|
140
|
+
│ └── EndpointDoc/ # One endpoint as a prose card
|
|
141
|
+
│ ├── index.tsx # Orchestrator: header + sections
|
|
142
|
+
│ ├── types.ts # SectionId union
|
|
143
|
+
│ ├── context.tsx # Identity context (endpointId + method)
|
|
144
|
+
│ │
|
|
145
|
+
│ ├── store/ # Zustand — open sections + active code tab
|
|
146
|
+
│ │ ├── index.ts # create() + sessionStorage persist
|
|
147
|
+
│ │ └── selectors.ts
|
|
148
|
+
│ │
|
|
149
|
+
│ ├── hooks/
|
|
150
|
+
│ │ └── useSectionHash.ts # #section=<id>.<sec> deep-link router
|
|
151
|
+
│ │
|
|
152
|
+
│ ├── Header/ # Meta row (badge + actions + Try it) + path
|
|
153
|
+
│ │ ├── index.tsx
|
|
154
|
+
│ │ ├── MetaActions.tsx # Copy link / markdown / expand all
|
|
155
|
+
│ │ ├── MethodBadge.tsx
|
|
156
|
+
│ │ └── PathDisplay.tsx
|
|
157
|
+
│ │
|
|
158
|
+
│ ├── Section/ # Collapsible section wrapper
|
|
159
|
+
│ │ ├── index.tsx
|
|
160
|
+
│ │ ├── SectionHeader.tsx # Chevron + title + badge + anchor
|
|
161
|
+
│ │ └── defaults.ts # Per-method open-by-default rules
|
|
162
|
+
│ │
|
|
163
|
+
│ ├── Parameters/ # Path + Query parameters block
|
|
164
|
+
│ │ ├── index.tsx
|
|
165
|
+
│ │ ├── ParamGroup.tsx
|
|
166
|
+
│ │ └── ParamRow.tsx
|
|
167
|
+
│ │
|
|
168
|
+
│ ├── RequestBody/ # Body preview
|
|
169
|
+
│ │ └── index.tsx
|
|
170
|
+
│ │
|
|
171
|
+
│ ├── SchemaFields/ # Tree-view of JSON Schema properties
|
|
172
|
+
│ │ ├── index.tsx
|
|
173
|
+
│ │ ├── FieldRow.tsx
|
|
174
|
+
│ │ ├── buildTree.ts
|
|
175
|
+
│ │ └── types.ts
|
|
176
|
+
│ │
|
|
177
|
+
│ ├── Responses/ # Status-code rows + example body
|
|
178
|
+
│ │ ├── index.tsx
|
|
179
|
+
│ │ ├── ResponseRow.tsx
|
|
180
|
+
│ │ ├── ResponseBody.tsx
|
|
181
|
+
│ │ └── StatusTag.tsx
|
|
182
|
+
│ │
|
|
183
|
+
│ └── CodeSamples/ # Language tabs + generated snippet
|
|
184
|
+
│ ├── index.tsx
|
|
185
|
+
│ ├── LanguageTabs.tsx
|
|
186
|
+
│ └── useCodeSnippet.ts
|
|
124
187
|
│
|
|
125
188
|
└── shared/ # Panels reused by SlideInPlayground + TryItSheet
|
|
126
189
|
├── RequestPanel.tsx
|
|
127
|
-
├── ResponsePanel.tsx
|
|
128
190
|
├── SendButton.tsx
|
|
129
191
|
├── BodyFormEditor.tsx # Recursive JSON-Schema form renderer
|
|
130
192
|
├── EndpointDraftSync.tsx# Headless draft ↔ context sync
|
|
131
193
|
├── EndpointResetButton.tsx
|
|
132
|
-
|
|
194
|
+
├── ui.tsx # MethodBadge, StatusBadge, Panel, atoms
|
|
195
|
+
│
|
|
196
|
+
└── ResponsePanel/ # Response viewer with Pretty / Raw / Preview tabs
|
|
197
|
+
├── index.tsx # Orchestrator + mode state + guards
|
|
198
|
+
├── types.ts # ViewMode + ContentKind
|
|
199
|
+
├── detectContent.ts # Content-Type → Prism language inference
|
|
200
|
+
├── useResponseView.ts
|
|
201
|
+
├── StatusBar.tsx # Status / size / duration / Copy
|
|
202
|
+
├── ViewTabs.tsx
|
|
203
|
+
├── PrettyView.tsx # JsonTree or syntax-highlighted code
|
|
204
|
+
├── RawView.tsx # Plain <pre>
|
|
205
|
+
└── PreviewView.tsx # Sandboxed iframe for HTML responses
|
|
133
206
|
```
|
|
134
207
|
|
|
208
|
+
## Endpoint doc — sections & deep-linking
|
|
209
|
+
|
|
210
|
+
Each endpoint card splits into four collapsible sections: **Parameters**,
|
|
211
|
+
**Request body**, **Code samples**, **Responses**. Defaults are
|
|
212
|
+
method-aware — GET opens Parameters + Responses, POST/PUT/PATCH opens
|
|
213
|
+
Request body + Responses, etc. User overrides are persisted per
|
|
214
|
+
endpoint in `sessionStorage` (zustand `persist` middleware) so a tab you
|
|
215
|
+
opened once stays open as you scroll.
|
|
216
|
+
|
|
217
|
+
A hover-revealed anchor button on each section header copies a shareable
|
|
218
|
+
URL like `…#section=ep-get-api-v3-pet-findbystatus.responses` —
|
|
219
|
+
following that link lands on the endpoint, expands the referenced
|
|
220
|
+
section, and scrolls it into view.
|
|
221
|
+
|
|
222
|
+
## Response panel — Pretty / Raw / Preview
|
|
223
|
+
|
|
224
|
+
The response panel picks a default view based on `Content-Type`:
|
|
225
|
+
|
|
226
|
+
- **JSON** → `Pretty` (interactive JsonTree)
|
|
227
|
+
- **HTML** → `Preview` (sandboxed iframe, scripts disabled) with `Pretty`
|
|
228
|
+
as the syntax-highlighted source fallback
|
|
229
|
+
- **XML / CSS / JS / text** → `Pretty` using Prism
|
|
230
|
+
- Everything has a **Raw** tab for a literal `<pre>` dump
|
|
231
|
+
|
|
232
|
+
The HTML preview detects single-page-app shells (empty `<body>` + mount
|
|
233
|
+
div + `<script>`) and shows an explanatory empty-state instead of a
|
|
234
|
+
blank iframe, since scripts can't run in the sandbox.
|
|
235
|
+
|
|
135
236
|
## Config Reference
|
|
136
237
|
|
|
137
238
|
```ts
|
|
@@ -145,6 +246,13 @@ interface PlaygroundConfig {
|
|
|
145
246
|
/** Optional API keys for the X-API-Key picker. */
|
|
146
247
|
apiKeys?: ApiKey[];
|
|
147
248
|
apiKeysLoading?: boolean;
|
|
249
|
+
/** Layout mode. ``'selector'`` (default) shows one schema at a time,
|
|
250
|
+
* picked via a Combobox. ``'sections'`` flattens every schema into
|
|
251
|
+
* the longread as top-level sections. */
|
|
252
|
+
schemaGrouping?: 'selector' | 'sections';
|
|
253
|
+
/** Sync the active endpoint anchor to ``window.location.hash`` as
|
|
254
|
+
* the user scrolls (sections mode). Default: off. */
|
|
255
|
+
urlSync?: boolean;
|
|
148
256
|
}
|
|
149
257
|
|
|
150
258
|
interface SchemaSource {
|
|
@@ -13,7 +13,21 @@ interface ApiIntroSectionProps {
|
|
|
13
13
|
resolvedBaseUrl?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface BaseUrlRow {
|
|
17
|
+
url: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }: ApiIntroSectionProps) {
|
|
22
|
+
// Prefer the *resolved* base URL whenever we have one — that's the
|
|
23
|
+
// URL actual requests target, not the raw ``servers[0].url`` from
|
|
24
|
+
// the spec (which can be a bare path like ``/api/v3``). Fall back
|
|
25
|
+
// to the spec's ``servers`` list so specs that document multiple
|
|
26
|
+
// servers keep showing all of them.
|
|
27
|
+
const baseUrlRows: BaseUrlRow[] = resolvedBaseUrl
|
|
28
|
+
? [{ url: resolvedBaseUrl, description: info.servers?.[0]?.description }]
|
|
29
|
+
: (info.servers ?? []).map((s) => ({ url: s.url, description: s.description }));
|
|
30
|
+
|
|
17
31
|
return (
|
|
18
32
|
<section className="pb-10 mb-10 border-b">
|
|
19
33
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
@@ -38,20 +52,20 @@ export function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }: Ap
|
|
|
38
52
|
</div>
|
|
39
53
|
)}
|
|
40
54
|
|
|
41
|
-
{
|
|
55
|
+
{baseUrlRows.length > 0 && (
|
|
42
56
|
<div className="mt-6 space-y-2">
|
|
43
57
|
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
44
58
|
Base URL
|
|
45
59
|
</h4>
|
|
46
60
|
<div className="space-y-1.5">
|
|
47
|
-
{
|
|
48
|
-
<div key={`${
|
|
61
|
+
{baseUrlRows.map((row, i) => (
|
|
62
|
+
<div key={`${row.url}-${i}`} className="flex items-baseline gap-2 flex-wrap">
|
|
49
63
|
<code className="font-mono text-xs px-2 py-1 rounded bg-muted border">
|
|
50
|
-
{
|
|
64
|
+
{row.url}
|
|
51
65
|
</code>
|
|
52
|
-
{
|
|
66
|
+
{row.description && (
|
|
53
67
|
<span className="text-xs text-muted-foreground">
|
|
54
|
-
{
|
|
68
|
+
{row.description}
|
|
55
69
|
</span>
|
|
56
70
|
)}
|
|
57
71
|
</div>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
15
15
|
import { ApiIntroSection } from './ApiIntroSection';
|
|
16
16
|
import { EndpointDoc } from './EndpointDoc';
|
|
17
|
+
import { useSectionHashRouter } from './EndpointDoc/hooks/useSectionHash';
|
|
17
18
|
import { SchemaCopyMenu } from './SchemaCopyMenu';
|
|
18
19
|
|
|
19
20
|
export interface DocsViewHandle {
|
|
@@ -150,6 +151,11 @@ export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function
|
|
|
150
151
|
const scrollTargetRef = useRef<ScrollTarget | null>(null);
|
|
151
152
|
const { onActiveChange } = props;
|
|
152
153
|
|
|
154
|
+
// ``#section=<endpointId>.<sectionId>`` shareable deep-links —
|
|
155
|
+
// opens the referenced section in the store and scrolls it in.
|
|
156
|
+
// Idempotent, attaches a single hashchange listener.
|
|
157
|
+
useSectionHashRouter();
|
|
158
|
+
|
|
153
159
|
// Resolve the real scroll container once the ref is attached. In
|
|
154
160
|
// standalone pages that's ``window``; inside an ``overflow-auto``
|
|
155
161
|
// shell (dev playground, modal) it's the wrapping DIV.
|
package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
import { CODE_SAMPLE_TARGETS, type CodeSampleTargetId } from '../../../../utils/codeSamples';
|
|
6
|
+
|
|
7
|
+
interface LanguageTabsProps {
|
|
8
|
+
activeId: CodeSampleTargetId;
|
|
9
|
+
onChange: (id: CodeSampleTargetId) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Horizontal tab strip for switching between cURL/JS/Python/… The
|
|
13
|
+
* strip scrolls horizontally on narrow viewports rather than wrapping
|
|
14
|
+
* to a second line so the adjacent code block keeps its vertical
|
|
15
|
+
* rhythm. */
|
|
16
|
+
export function LanguageTabs({ activeId, onChange }: LanguageTabsProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center gap-1 overflow-x-auto -mx-1 px-1">
|
|
19
|
+
{CODE_SAMPLE_TARGETS.map((t) => (
|
|
20
|
+
<button
|
|
21
|
+
key={t.id}
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => onChange(t.id)}
|
|
24
|
+
className={cn(
|
|
25
|
+
'shrink-0 h-7 px-2.5 rounded text-xs font-medium transition-colors',
|
|
26
|
+
activeId === t.id
|
|
27
|
+
? 'bg-muted text-foreground'
|
|
28
|
+
: 'text-muted-foreground/70 hover:text-foreground hover:bg-muted/50',
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{t.label}
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import PrettyCode from '../../../../../PrettyCode';
|
|
4
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
5
|
+
import { useEndpointDocContext } from '../context';
|
|
6
|
+
import { useEndpointDocStore } from '../store';
|
|
7
|
+
import { useActiveCodeTab } from '../store/selectors';
|
|
8
|
+
import { LanguageTabs } from './LanguageTabs';
|
|
9
|
+
import { useCodeSnippet } from './useCodeSnippet';
|
|
10
|
+
|
|
11
|
+
interface CodeSamplesProps {
|
|
12
|
+
endpoint: ApiEndpoint;
|
|
13
|
+
/** Optional body to include in generated snippets. When omitted we
|
|
14
|
+
* use ``endpoint.requestBody?.example`` if present, so the snippet
|
|
15
|
+
* shows a realistic payload out of the box. */
|
|
16
|
+
body?: string;
|
|
17
|
+
/** Parameter values to substitute into the URL. Missing path params
|
|
18
|
+
* fall back to ``{name}`` placeholders so the snippet still
|
|
19
|
+
* illustrates the shape. */
|
|
20
|
+
parameters?: Record<string, string>;
|
|
21
|
+
/** Extra headers to include in snippets (e.g. a picked API key). */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** Base URL override — falls back to ``endpoint.path`` which
|
|
24
|
+
* already has the resolved base URL prepended by the extractor. */
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Code samples block: language tab bar + highlighted snippet. The
|
|
29
|
+
* outer Section wrapper (collapsible) lives one level up; this
|
|
30
|
+
* component is always "open" from its own perspective. */
|
|
31
|
+
export function CodeSamples({ endpoint, body, parameters, headers, baseUrl }: CodeSamplesProps) {
|
|
32
|
+
const { endpointId } = useEndpointDocContext();
|
|
33
|
+
const activeId = useActiveCodeTab(endpointId);
|
|
34
|
+
const setCodeTab = useEndpointDocStore((s) => s.setCodeTab);
|
|
35
|
+
|
|
36
|
+
const { snippet, prism } = useCodeSnippet({
|
|
37
|
+
endpoint,
|
|
38
|
+
body,
|
|
39
|
+
parameters,
|
|
40
|
+
headers,
|
|
41
|
+
baseUrl,
|
|
42
|
+
activeId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-2.5">
|
|
47
|
+
<LanguageTabs activeId={activeId} onChange={(id) => setCodeTab(endpointId, id)} />
|
|
48
|
+
<PrettyCode
|
|
49
|
+
data={snippet}
|
|
50
|
+
language={prism as never}
|
|
51
|
+
isCompact
|
|
52
|
+
maxLines={20}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
6
|
+
import {
|
|
7
|
+
CODE_SAMPLE_TARGETS,
|
|
8
|
+
renderSnippet,
|
|
9
|
+
type CodeSampleTargetId,
|
|
10
|
+
} from '../../../../utils/codeSamples';
|
|
11
|
+
import { buildHarRequest } from '../../../../utils/operationToHar';
|
|
12
|
+
import { resolveAbsolute } from '../../../../utils/url';
|
|
13
|
+
|
|
14
|
+
interface UseCodeSnippetInput {
|
|
15
|
+
endpoint: ApiEndpoint;
|
|
16
|
+
body?: string;
|
|
17
|
+
parameters?: Record<string, string>;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
activeId: CodeSampleTargetId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface UseCodeSnippetResult {
|
|
24
|
+
/** Fully-rendered snippet string. Always present — falls back to an
|
|
25
|
+
* "unavailable" message if the generator returns null, so the
|
|
26
|
+
* consumer can always mount ``PrettyCode``. */
|
|
27
|
+
snippet: string;
|
|
28
|
+
/** Prism language id matching ``activeId`` — passed to PrettyCode so
|
|
29
|
+
* it picks the right highlighter. */
|
|
30
|
+
prism: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Encapsulates HAR build + snippet render + memoisation for the Code
|
|
34
|
+
* Samples block. Kept as a hook (rather than inline ``useMemo`` blocks
|
|
35
|
+
* in the component) so unit tests can exercise the snippet pipeline
|
|
36
|
+
* independently of React rendering. */
|
|
37
|
+
export function useCodeSnippet({
|
|
38
|
+
endpoint,
|
|
39
|
+
body,
|
|
40
|
+
parameters,
|
|
41
|
+
headers,
|
|
42
|
+
baseUrl,
|
|
43
|
+
activeId,
|
|
44
|
+
}: UseCodeSnippetInput): UseCodeSnippetResult {
|
|
45
|
+
const effectiveBody = body ?? endpoint.requestBody?.example;
|
|
46
|
+
|
|
47
|
+
// Build the HAR once per input change — every tab rebuilds its
|
|
48
|
+
// snippet from this shared request shape.
|
|
49
|
+
//
|
|
50
|
+
// ``endpoint.path`` already carries the schema's ``servers[0].url``
|
|
51
|
+
// (joined upstream in ``useOpenApiSchema``). Usually that's a path
|
|
52
|
+
// like ``/api/v3/pet`` — good enough for same-origin fetch, wrong
|
|
53
|
+
// for curl/python/go which need a runnable absolute URL. We hand
|
|
54
|
+
// off to ``resolveAbsolute`` so the snippet is copy-pasteable from
|
|
55
|
+
// a terminal without the user having to edit the host in manually.
|
|
56
|
+
//
|
|
57
|
+
// Priority: explicit ``baseUrl`` prop > resolved origin > bare path.
|
|
58
|
+
const har = useMemo(() => {
|
|
59
|
+
const h = buildHarRequest({
|
|
60
|
+
endpoint,
|
|
61
|
+
body: effectiveBody,
|
|
62
|
+
parameters,
|
|
63
|
+
headers,
|
|
64
|
+
baseUrl,
|
|
65
|
+
});
|
|
66
|
+
return baseUrl ? h : { ...h, url: resolveAbsolute(h.url) };
|
|
67
|
+
}, [endpoint, effectiveBody, parameters, headers, baseUrl]);
|
|
68
|
+
|
|
69
|
+
return useMemo(() => {
|
|
70
|
+
const target = CODE_SAMPLE_TARGETS.find((t) => t.id === activeId)!;
|
|
71
|
+
const code = renderSnippet(har, activeId);
|
|
72
|
+
return {
|
|
73
|
+
snippet: code ?? `// Snippet for ${target.label} is unavailable for this request.`,
|
|
74
|
+
prism: target.prism,
|
|
75
|
+
};
|
|
76
|
+
}, [har, activeId]);
|
|
77
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Check,
|
|
5
|
+
ChevronsDownUp,
|
|
6
|
+
ChevronsUpDown,
|
|
7
|
+
FileCode2,
|
|
8
|
+
Link2,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Tooltip,
|
|
14
|
+
TooltipContent,
|
|
15
|
+
TooltipTrigger,
|
|
16
|
+
SafeTooltipProvider,
|
|
17
|
+
} from '@djangocfg/ui-core/components';
|
|
18
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
19
|
+
|
|
20
|
+
import { useEndpointDocContext } from '../context';
|
|
21
|
+
import { sectionKey, useEndpointDocStore } from '../store';
|
|
22
|
+
import type { SectionId } from '../types';
|
|
23
|
+
|
|
24
|
+
interface MetaActionsProps {
|
|
25
|
+
anchor: string;
|
|
26
|
+
endpointMarkdown: string;
|
|
27
|
+
/** Sections present on this endpoint — expand/collapse acts only
|
|
28
|
+
* on visible rows, never on catalogue items the card doesn't render. */
|
|
29
|
+
presentSections: readonly SectionId[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IconButtonProps {
|
|
33
|
+
label: string;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
active?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Tight 24×24 icon button used across the meta row. Keeps the hit
|
|
40
|
+
* target compact so the row reads as a secondary metadata strip, not
|
|
41
|
+
* a toolbar that competes with the path for attention. */
|
|
42
|
+
function IconButton({ label, onClick, children, active }: IconButtonProps) {
|
|
43
|
+
return (
|
|
44
|
+
<Tooltip>
|
|
45
|
+
<TooltipTrigger asChild>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
aria-label={label}
|
|
50
|
+
className={cn(
|
|
51
|
+
'shrink-0 h-6 w-6 inline-flex items-center justify-center rounded',
|
|
52
|
+
'text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors',
|
|
53
|
+
active && 'text-emerald-500 hover:text-emerald-500',
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</button>
|
|
58
|
+
</TooltipTrigger>
|
|
59
|
+
<TooltipContent side="bottom" className="text-[11px]">
|
|
60
|
+
{label}
|
|
61
|
+
</TooltipContent>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Inline meta-row actions: copy link · copy markdown · expand/collapse
|
|
67
|
+
* all. Actions are always visible (corporate tool pattern) but sized
|
|
68
|
+
* down so the path on the next line stays the visual focal point. */
|
|
69
|
+
export function MetaActions({ anchor, endpointMarkdown, presentSections }: MetaActionsProps) {
|
|
70
|
+
const { endpointId } = useEndpointDocContext();
|
|
71
|
+
const expandAll = useEndpointDocStore((s) => s.expandAll);
|
|
72
|
+
const collapseAll = useEndpointDocStore((s) => s.collapseAll);
|
|
73
|
+
const openSections = useEndpointDocStore((s) => s.openSections);
|
|
74
|
+
|
|
75
|
+
const [justCopied, setJustCopied] = useState<'link' | 'md' | null>(null);
|
|
76
|
+
const flash = useCallback((which: 'link' | 'md') => {
|
|
77
|
+
setJustCopied(which);
|
|
78
|
+
setTimeout(() => setJustCopied(null), 1200);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const mostlyOpen = useMemo(() => {
|
|
82
|
+
if (presentSections.length === 0) return false;
|
|
83
|
+
let openCount = 0;
|
|
84
|
+
for (const sid of presentSections) {
|
|
85
|
+
if (openSections[sectionKey(endpointId, sid)]) openCount += 1;
|
|
86
|
+
}
|
|
87
|
+
return openCount > presentSections.length / 2;
|
|
88
|
+
}, [openSections, presentSections, endpointId]);
|
|
89
|
+
|
|
90
|
+
const copyLink = useCallback(() => {
|
|
91
|
+
if (typeof window === 'undefined') return;
|
|
92
|
+
const url = `${window.location.origin}${window.location.pathname}#${anchor}`;
|
|
93
|
+
void navigator.clipboard?.writeText(url).then(() => flash('link'));
|
|
94
|
+
}, [anchor, flash]);
|
|
95
|
+
|
|
96
|
+
const copyMarkdown = useCallback(() => {
|
|
97
|
+
if (typeof window === 'undefined') return;
|
|
98
|
+
void navigator.clipboard?.writeText(endpointMarkdown).then(() => flash('md'));
|
|
99
|
+
}, [endpointMarkdown, flash]);
|
|
100
|
+
|
|
101
|
+
const toggleAll = useCallback(() => {
|
|
102
|
+
if (mostlyOpen) collapseAll(endpointId, presentSections);
|
|
103
|
+
else expandAll(endpointId, presentSections);
|
|
104
|
+
}, [mostlyOpen, collapseAll, expandAll, endpointId, presentSections]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<SafeTooltipProvider delayDuration={200}>
|
|
108
|
+
<div className="flex items-center gap-0.5">
|
|
109
|
+
<IconButton
|
|
110
|
+
label={justCopied === 'link' ? 'Copied!' : 'Copy link to endpoint'}
|
|
111
|
+
onClick={copyLink}
|
|
112
|
+
active={justCopied === 'link'}
|
|
113
|
+
>
|
|
114
|
+
{justCopied === 'link' ? (
|
|
115
|
+
<Check className="h-3.5 w-3.5" />
|
|
116
|
+
) : (
|
|
117
|
+
<Link2 className="h-3.5 w-3.5" />
|
|
118
|
+
)}
|
|
119
|
+
</IconButton>
|
|
120
|
+
<IconButton
|
|
121
|
+
label={justCopied === 'md' ? 'Copied!' : 'Copy as Markdown (for AI)'}
|
|
122
|
+
onClick={copyMarkdown}
|
|
123
|
+
active={justCopied === 'md'}
|
|
124
|
+
>
|
|
125
|
+
{justCopied === 'md' ? (
|
|
126
|
+
<Check className="h-3.5 w-3.5" />
|
|
127
|
+
) : (
|
|
128
|
+
<FileCode2 className="h-3.5 w-3.5" />
|
|
129
|
+
)}
|
|
130
|
+
</IconButton>
|
|
131
|
+
{presentSections.length >= 2 && (
|
|
132
|
+
<IconButton
|
|
133
|
+
label={mostlyOpen ? 'Collapse all sections' : 'Expand all sections'}
|
|
134
|
+
onClick={toggleAll}
|
|
135
|
+
>
|
|
136
|
+
{mostlyOpen ? (
|
|
137
|
+
<ChevronsDownUp className="h-3.5 w-3.5" />
|
|
138
|
+
) : (
|
|
139
|
+
<ChevronsUpDown className="h-3.5 w-3.5" />
|
|
140
|
+
)}
|
|
141
|
+
</IconButton>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</SafeTooltipProvider>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Thin re-export so the Header barrel file stays self-contained. The
|
|
4
|
+
// actual badge lives in ``shared/ui`` because it's used by the sidebar
|
|
5
|
+
// and the playground too — duplicating styles would drift.
|
|
6
|
+
export { MethodBadge } from '../../../shared/ui';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { relativePath } from '../../../shared/ui';
|
|
4
|
+
|
|
5
|
+
interface PathDisplayProps {
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The endpoint path as the visual focal point of the card. Renders in
|
|
10
|
+
* large monospace so readers scanning the page land on it first — the
|
|
11
|
+
* path is what distinguishes one endpoint from another, so it should
|
|
12
|
+
* be the biggest visible element short of the section title hierarchy.
|
|
13
|
+
*
|
|
14
|
+
* ``relativePath`` strips the resolved base URL so the docs always
|
|
15
|
+
* show the API-local path, not an absolute URL cluttered with host
|
|
16
|
+
* noise. */
|
|
17
|
+
export function PathDisplay({ path }: PathDisplayProps) {
|
|
18
|
+
return (
|
|
19
|
+
<code
|
|
20
|
+
className="block font-mono text-lg md:text-xl font-semibold text-foreground leading-tight"
|
|
21
|
+
style={{ overflowWrap: 'anywhere', wordBreak: 'break-word' }}
|
|
22
|
+
>
|
|
23
|
+
{relativePath(path)}
|
|
24
|
+
</code>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Play } from 'lucide-react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
|
|
8
|
+
import { MarkdownMessage } from '../../../../../../components/markdown';
|
|
9
|
+
import type { ApiEndpoint } from '../../../../types';
|
|
10
|
+
import { endpointToMarkdown } from '../../../../utils/schemaExport';
|
|
11
|
+
import type { SectionId } from '../types';
|
|
12
|
+
import { MetaActions } from './MetaActions';
|
|
13
|
+
import { MethodBadge } from './MethodBadge';
|
|
14
|
+
import { PathDisplay } from './PathDisplay';
|
|
15
|
+
|
|
16
|
+
interface EndpointHeaderProps {
|
|
17
|
+
endpoint: ApiEndpoint;
|
|
18
|
+
anchor: string;
|
|
19
|
+
isLoadedInPlayground: boolean;
|
|
20
|
+
onTryIt: () => void;
|
|
21
|
+
/** Sections actually rendered on this endpoint — drives the
|
|
22
|
+
* expand/collapse-all icon in the meta row. */
|
|
23
|
+
presentSections: readonly SectionId[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Card header, three stacked rows:
|
|
27
|
+
* 1. Meta — small utility strip: method badge · inline actions · Try-it.
|
|
28
|
+
* 2. Path — large monospace, the visual focus of the card.
|
|
29
|
+
* 3. Description — prose, aligned to the left edge under path.
|
|
30
|
+
*
|
|
31
|
+
* Splitting metadata from the path lets the path itself become the
|
|
32
|
+
* focal point — readers scan paths when scrolling, not badges. The
|
|
33
|
+
* meta row stays compact so it reads as "info about this endpoint"
|
|
34
|
+
* rather than "toolbar that competes for attention". */
|
|
35
|
+
export function EndpointHeader({
|
|
36
|
+
endpoint,
|
|
37
|
+
anchor,
|
|
38
|
+
isLoadedInPlayground,
|
|
39
|
+
onTryIt,
|
|
40
|
+
presentSections,
|
|
41
|
+
}: EndpointHeaderProps) {
|
|
42
|
+
// Memoise the markdown dump — only recomputes when the endpoint
|
|
43
|
+
// reference changes, not on unrelated re-renders of the subtree.
|
|
44
|
+
const endpointMd = useMemo(() => endpointToMarkdown(endpoint), [endpoint]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<header className="space-y-3">
|
|
48
|
+
{/* Row 1 — meta strip. Badge + inline icon actions on the
|
|
49
|
+
left, primary CTA on the right. Kept tight (24px tall)
|
|
50
|
+
so it doesn't visually compete with the path row below. */}
|
|
51
|
+
<div className="flex items-center gap-3 flex-wrap">
|
|
52
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
53
|
+
<MethodBadge method={endpoint.method} />
|
|
54
|
+
<MetaActions
|
|
55
|
+
anchor={anchor}
|
|
56
|
+
endpointMarkdown={endpointMd}
|
|
57
|
+
presentSections={presentSections}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<Button
|
|
61
|
+
size="sm"
|
|
62
|
+
variant={isLoadedInPlayground ? 'secondary' : 'default'}
|
|
63
|
+
onClick={onTryIt}
|
|
64
|
+
className="ml-auto h-7 text-xs gap-1.5 px-2.5"
|
|
65
|
+
>
|
|
66
|
+
<Play className="h-3 w-3" />
|
|
67
|
+
{isLoadedInPlayground ? 'Loaded' : 'Try it'}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Row 2 — path as the visual focal point. Larger and more
|
|
72
|
+
prominent than it was when competing with the badge
|
|
73
|
+
and action icons inline. */}
|
|
74
|
+
<div className="min-w-0">
|
|
75
|
+
<PathDisplay path={endpoint.path} />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Row 3 — description, aligned to the left edge under the
|
|
79
|
+
path. Text-sm so it reads as subtitle, not body copy. */}
|
|
80
|
+
{endpoint.description && (
|
|
81
|
+
<div className="text-muted-foreground text-sm">
|
|
82
|
+
<MarkdownMessage content={endpoint.description} />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</header>
|
|
86
|
+
);
|
|
87
|
+
}
|