@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.
Files changed (98) hide show
  1. package/README.md +14 -3
  2. package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
  3. package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
  4. package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
  5. package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
  6. package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
  7. package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
  9. package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
  10. package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
  11. package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
  12. package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
  13. package/dist/chunk-EFWOJPA6.mjs.map +1 -0
  14. package/dist/index.cjs +18 -10
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +35 -1
  17. package/dist/index.d.ts +35 -1
  18. package/dist/index.mjs +13 -5
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +20 -15
  21. package/src/components/markdown/MarkdownMessage.tsx +46 -0
  22. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
  23. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
  24. package/src/tools/OpenapiViewer/README.md +114 -6
  25. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
  26. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
  27. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
  39. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
  40. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
  41. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
  42. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
  43. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
  44. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
  45. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
  46. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
  47. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
  48. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
  49. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
  50. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
  51. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
  52. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
  53. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
  54. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
  55. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
  56. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
  57. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
  58. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
  59. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
  60. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
  61. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
  62. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
  63. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
  64. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
  65. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
  66. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
  67. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
  68. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
  69. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
  70. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
  71. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
  72. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
  73. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
  74. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
  75. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
  76. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
  77. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
  78. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
  79. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
  80. package/src/tools/OpenapiViewer/types.ts +10 -0
  81. package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
  82. package/src/tools/OpenapiViewer/utils/index.ts +3 -0
  83. package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
  84. package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
  85. package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
  86. package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
  87. package/src/tools/PrettyCode/index.tsx +13 -0
  88. package/src/tools/PrettyCode/lazy.tsx +5 -0
  89. package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
  90. package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
  91. package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
  92. package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
  93. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
  94. package/dist/chunk-IULI4XII.cjs.map +0 -1
  95. package/dist/chunk-VZGQC3NG.mjs.map +0 -1
  96. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
  97. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
  98. 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
- └── sidebarLabel.ts # Common-prefix strip + tooltip helpers
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
- └── ui.tsx # MethodBadge, StatusBadge, Panel, atoms
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
- {info.servers && info.servers.length > 0 && (
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
- {info.servers.map((s, i) => (
48
- <div key={`${s.url}-${i}`} className="flex items-baseline gap-2 flex-wrap">
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
- {s.url}
64
+ {row.url}
51
65
  </code>
52
- {s.description && (
66
+ {row.description && (
53
67
  <span className="text-xs text-muted-foreground">
54
- {s.description}
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.
@@ -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
+ }
@@ -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
+ }