@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
@@ -1,439 +0,0 @@
1
- 'use client';
2
-
3
- import { Search } from 'lucide-react';
4
- import React, { useEffect, useMemo, useState } from 'react';
5
-
6
- import {
7
- Combobox,
8
- Input,
9
- Tooltip,
10
- TooltipContent,
11
- TooltipTrigger,
12
- } from '@djangocfg/ui-core/components';
13
- import { cn } from '@djangocfg/ui-core/lib';
14
-
15
- import type { ApiEndpoint, OpenApiInfo, OpenApiSchema, SchemaSource } from '../../types';
16
- import { deduplicateEndpoints } from '../../utils/versionManager';
17
- import { MethodBadge, ScrollArea } from '../shared/ui';
18
- import { endpointAnchor } from './anchor';
19
- import { buildSchemaSections, groupEndpoints, type EndpointGroup } from './grouping';
20
- import { SchemaCopyMenu } from './SchemaCopyMenu';
21
- import { sidebarLabel, sidebarTooltip } from './sidebarLabel';
22
-
23
- // ─── Public props ────────────────────────────────────────────────────────────
24
-
25
- export interface DocsSidebarProps {
26
- info: OpenApiInfo | null;
27
- /** Active-schema endpoints — used by ``selector`` mode. */
28
- endpoints: ApiEndpoint[];
29
- /** All configured schemas (used by both modes). */
30
- schemas: SchemaSource[];
31
- currentSchemaId: string | null;
32
- onSchemaChange: (id: string) => void;
33
- activeEndpointId: string | null;
34
- selectedVersion: string;
35
- onNavigate: (anchor: string, schemaId?: string | null) => void;
36
- /** Presentation mode. Default: ``selector`` (back-compat). */
37
- grouping?: 'selector' | 'sections';
38
- /** Required for ``sections`` mode — endpoints grouped by their source
39
- * schema id. The sidebar renders them as two-level sections. */
40
- endpointsBySchema?: Record<string, ApiEndpoint[]>;
41
- /** Raw active schema + resolved base URL — used by the Copy-for-AI
42
- * dropdown in the brand row. ``null`` disables the button. */
43
- rawSchema?: OpenApiSchema | null;
44
- resolvedBaseUrl?: string;
45
- }
46
-
47
- /** HTTP methods the sidebar can filter by. Reordered like the docs
48
- * themselves: read, then write. ``ALL`` is a sentinel for no filter. */
49
- const METHOD_FILTERS = ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
50
- type MethodFilter = (typeof METHOD_FILTERS)[number];
51
-
52
- // ─── View-model types ────────────────────────────────────────────────────────
53
-
54
- interface EndpointRowVM {
55
- key: string;
56
- anchor: string;
57
- schemaId: string | null;
58
- label: string;
59
- tooltip: string;
60
- method: string;
61
- /** Summary-less endpoints get a monospace font for the path-tail label. */
62
- useMono: boolean;
63
- isActive: boolean;
64
- }
65
-
66
- interface CategoryVM {
67
- key: string;
68
- category: string;
69
- rows: EndpointRowVM[];
70
- }
71
-
72
- interface SchemaSectionVM {
73
- sourceId: string;
74
- sourceName: string;
75
- categories: CategoryVM[];
76
- }
77
-
78
- type SidebarBodyVM =
79
- | { kind: 'flat'; categories: CategoryVM[]; emptyText: string }
80
- | { kind: 'sections'; sections: SchemaSectionVM[]; emptyText: string };
81
-
82
- // ─── Pure builders ───────────────────────────────────────────────────────────
83
-
84
- function filterEndpoints(
85
- list: ApiEndpoint[],
86
- query: string,
87
- method: MethodFilter,
88
- ): ApiEndpoint[] {
89
- let out = list;
90
- if (method !== 'ALL') {
91
- out = out.filter((e) => e.method === method);
92
- }
93
- if (query) {
94
- const q = query.toLowerCase();
95
- out = out.filter(
96
- (e) =>
97
- e.summary.toLowerCase().includes(q) ||
98
- e.name.toLowerCase().includes(q) ||
99
- e.description.toLowerCase().includes(q) ||
100
- e.path.toLowerCase().includes(q),
101
- );
102
- }
103
- return out;
104
- }
105
-
106
- function buildCategory(
107
- group: EndpointGroup,
108
- activeEndpointId: string | null,
109
- schemaId: string | null,
110
- keyPrefix: string,
111
- ): CategoryVM {
112
- const rows: EndpointRowVM[] = group.endpoints.map((ep) => {
113
- const anchor = endpointAnchor(ep, schemaId ?? ep.schemaId ?? null);
114
- return {
115
- key: `${ep.method}-${ep.path}`,
116
- anchor,
117
- schemaId: schemaId ?? ep.schemaId ?? null,
118
- label: sidebarLabel(ep, group.commonPrefix),
119
- tooltip: sidebarTooltip(ep),
120
- method: ep.method,
121
- useMono: !ep.summary,
122
- isActive: activeEndpointId === anchor,
123
- };
124
- });
125
- return {
126
- key: `${keyPrefix}${group.category}`,
127
- category: group.category,
128
- rows,
129
- };
130
- }
131
-
132
- const emptyTextFor = (query: string, method: MethodFilter, defaultText: string): string => {
133
- if (query && method !== 'ALL') return `No ${method} endpoints match "${query}"`;
134
- if (query) return `No endpoints match "${query}"`;
135
- if (method !== 'ALL') return `No ${method} endpoints`;
136
- return defaultText;
137
- };
138
-
139
- function buildFlatVM(
140
- endpoints: ApiEndpoint[],
141
- selectedVersion: string,
142
- query: string,
143
- method: MethodFilter,
144
- activeEndpointId: string | null,
145
- ): SidebarBodyVM {
146
- const filtered = filterEndpoints(deduplicateEndpoints(endpoints, selectedVersion), query, method);
147
- const groups = groupEndpoints(filtered);
148
- return {
149
- kind: 'flat',
150
- categories: groups.map((g) => buildCategory(g, activeEndpointId, null, '')),
151
- emptyText: emptyTextFor(query, method, 'No endpoints in this schema'),
152
- };
153
- }
154
-
155
- function buildSectionsVM(
156
- schemas: SchemaSource[],
157
- endpointsBySchema: Record<string, ApiEndpoint[]>,
158
- selectedVersion: string,
159
- query: string,
160
- method: MethodFilter,
161
- activeEndpointId: string | null,
162
- ): SidebarBodyVM {
163
- const filteredMap: Record<string, ApiEndpoint[]> = {};
164
- for (const src of schemas) {
165
- const raw = endpointsBySchema[src.id] ?? [];
166
- filteredMap[src.id] = filterEndpoints(deduplicateEndpoints(raw, selectedVersion), query, method);
167
- }
168
- const rawSections = buildSchemaSections(schemas, filteredMap);
169
- const sections: SchemaSectionVM[] = rawSections
170
- .filter((s) => s.groups.length > 0)
171
- .map((s) => ({
172
- sourceId: s.source.id,
173
- sourceName: s.source.name,
174
- categories: s.groups.map((g) => buildCategory(g, activeEndpointId, s.source.id, `${s.source.id}-`)),
175
- }));
176
- return {
177
- kind: 'sections',
178
- sections,
179
- emptyText: emptyTextFor(query, method, 'No endpoints in any schema'),
180
- };
181
- }
182
-
183
- // ─── Component ───────────────────────────────────────────────────────────────
184
-
185
- export function DocsSidebar({
186
- info,
187
- endpoints,
188
- schemas,
189
- currentSchemaId,
190
- onSchemaChange,
191
- activeEndpointId,
192
- selectedVersion,
193
- onNavigate,
194
- grouping = 'selector',
195
- endpointsBySchema,
196
- rawSchema,
197
- resolvedBaseUrl,
198
- }: DocsSidebarProps) {
199
- const [search, setSearch] = useState('');
200
- const [debounced, setDebounced] = useState('');
201
- const [methodFilter, setMethodFilter] = useState<MethodFilter>('ALL');
202
-
203
- useEffect(() => {
204
- const id = setTimeout(() => setDebounced(search), 120);
205
- return () => clearTimeout(id);
206
- }, [search]);
207
-
208
- const body = useMemo<SidebarBodyVM>(() => {
209
- if (grouping === 'sections') {
210
- return buildSectionsVM(
211
- schemas,
212
- endpointsBySchema ?? {},
213
- selectedVersion,
214
- debounced,
215
- methodFilter,
216
- activeEndpointId,
217
- );
218
- }
219
- return buildFlatVM(endpoints, selectedVersion, debounced, methodFilter, activeEndpointId);
220
- }, [
221
- grouping,
222
- schemas,
223
- endpointsBySchema,
224
- endpoints,
225
- selectedVersion,
226
- debounced,
227
- methodFilter,
228
- activeEndpointId,
229
- ]);
230
-
231
- const schemaOptions = useMemo(
232
- () => schemas.map((s) => ({ value: s.id, label: s.name })),
233
- [schemas],
234
- );
235
- const hasMultipleSchemas = schemas.length > 1;
236
- const apiTitle = info?.title ?? 'API Reference';
237
- const showCombobox = grouping === 'selector' && hasMultipleSchemas;
238
- const copyReady = rawSchema !== null && rawSchema !== undefined && endpoints.length > 0;
239
-
240
- return (
241
- <aside className="flex flex-col h-full min-h-0 border-r bg-muted/10">
242
- {/* Brand row */}
243
- <div className="shrink-0 border-b px-3 h-12 flex items-center gap-2">
244
- <div className="flex items-center gap-2 flex-1 min-w-0">
245
- <span className="text-[13px] font-semibold text-foreground truncate">
246
- {apiTitle}
247
- </span>
248
- {info?.version && (
249
- <span className="font-mono text-[10px] text-muted-foreground/70 shrink-0">
250
- v{info.version}
251
- </span>
252
- )}
253
- </div>
254
- {copyReady && (
255
- <SchemaCopyMenu
256
- schema={rawSchema ?? null}
257
- endpoints={endpoints}
258
- baseUrl={resolvedBaseUrl}
259
- variant="icon"
260
- />
261
- )}
262
- </div>
263
-
264
- {/* Controls */}
265
- <div className="shrink-0 border-b px-3 py-3 space-y-2">
266
- {showCombobox && (
267
- <Combobox
268
- options={schemaOptions}
269
- value={currentSchemaId ?? ''}
270
- onValueChange={(id) => id && onSchemaChange(id)}
271
- placeholder="Select API"
272
- searchPlaceholder="Search APIs…"
273
- emptyText="No APIs found"
274
- className="w-full h-8 text-xs"
275
- />
276
- )}
277
- <div className="relative">
278
- <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" />
279
- <Input
280
- placeholder="Search endpoints…"
281
- value={search}
282
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
283
- className="pl-8 h-8 text-xs"
284
- />
285
- </div>
286
- <MethodChips value={methodFilter} onChange={setMethodFilter} />
287
- </div>
288
-
289
- <ScrollArea>
290
- <SidebarBody body={body} onNavigate={onNavigate} />
291
- </ScrollArea>
292
- </aside>
293
- );
294
- }
295
-
296
- function MethodChips({
297
- value,
298
- onChange,
299
- }: {
300
- value: MethodFilter;
301
- onChange: (v: MethodFilter) => void;
302
- }) {
303
- return (
304
- <div className="flex items-center gap-1 flex-wrap">
305
- {METHOD_FILTERS.map((m) => {
306
- const active = value === m;
307
- return (
308
- <button
309
- key={m}
310
- type="button"
311
- onClick={() => onChange(m)}
312
- aria-pressed={active}
313
- className={cn(
314
- 'px-2 py-0.5 rounded font-mono text-[10px] font-semibold tracking-wide transition-colors border',
315
- active
316
- ? 'bg-primary/15 border-primary/40 text-foreground'
317
- : 'bg-transparent border-border/40 text-muted-foreground hover:text-foreground hover:border-border',
318
- )}
319
- >
320
- {m}
321
- </button>
322
- );
323
- })}
324
- </div>
325
- );
326
- }
327
-
328
- // ─── Rendering primitives ────────────────────────────────────────────────────
329
-
330
- type NavigateFn = (anchor: string, schemaId?: string | null) => void;
331
-
332
- function SidebarBody({ body, onNavigate }: { body: SidebarBodyVM; onNavigate: NavigateFn }) {
333
- if (body.kind === 'flat') {
334
- if (body.categories.length === 0) {
335
- return (
336
- <div className="py-10 px-4 text-center text-xs text-muted-foreground">
337
- {body.emptyText}
338
- </div>
339
- );
340
- }
341
- return (
342
- <nav className="py-2">
343
- {body.categories.map((cat) => (
344
- <CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
345
- ))}
346
- </nav>
347
- );
348
- }
349
-
350
- if (body.sections.length === 0) {
351
- return (
352
- <div className="py-10 px-4 text-center text-xs text-muted-foreground">
353
- {body.emptyText}
354
- </div>
355
- );
356
- }
357
-
358
- return (
359
- <nav className="py-2">
360
- {body.sections.map((section) => (
361
- <div key={section.sourceId} className="mb-5 last:mb-2">
362
- <div className="px-4 py-2 sticky top-0 z-[1] bg-muted/30 backdrop-blur-[2px] border-b border-border/30">
363
- <span className="text-[11px] font-bold uppercase tracking-[0.12em] text-foreground/80">
364
- {section.sourceName}
365
- </span>
366
- </div>
367
- {section.categories.map((cat) => (
368
- <CategoryBlock key={cat.key} category={cat} onNavigate={onNavigate} />
369
- ))}
370
- </div>
371
- ))}
372
- </nav>
373
- );
374
- }
375
-
376
- const CategoryBlock = React.memo(function CategoryBlock({
377
- category,
378
- onNavigate,
379
- }: {
380
- category: CategoryVM;
381
- onNavigate: NavigateFn;
382
- }) {
383
- return (
384
- <div className="mb-4 last:mb-2">
385
- <div className="px-4 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/50 select-none">
386
- {category.category}
387
- </div>
388
- <div>
389
- {category.rows.map((row) => (
390
- <EndpointRow key={row.key} row={row} onNavigate={onNavigate} />
391
- ))}
392
- </div>
393
- </div>
394
- );
395
- });
396
-
397
- const EndpointRow = React.memo(function EndpointRow({
398
- row,
399
- onNavigate,
400
- }: {
401
- row: EndpointRowVM;
402
- onNavigate: NavigateFn;
403
- }) {
404
- return (
405
- <Tooltip delayDuration={350}>
406
- <TooltipTrigger asChild>
407
- <button
408
- onClick={() => onNavigate(row.anchor, row.schemaId)}
409
- aria-current={row.isActive ? 'location' : undefined}
410
- className={cn(
411
- 'relative group w-full text-left flex items-start gap-2 pl-4 pr-3 py-1.5 transition-colors',
412
- row.isActive
413
- ? 'bg-primary/10 text-foreground'
414
- : 'hover:bg-muted/40 text-foreground/75 hover:text-foreground',
415
- )}
416
- >
417
- {row.isActive && (
418
- <span className="absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-primary" />
419
- )}
420
- <span className="shrink-0 mt-[1px]">
421
- <MethodBadge method={row.method} />
422
- </span>
423
- <span
424
- className={cn(
425
- 'line-clamp-2 leading-snug flex-1 min-w-0',
426
- row.useMono ? 'font-mono text-[11px] break-all' : 'text-[12px]',
427
- row.isActive && 'text-foreground font-medium',
428
- )}
429
- >
430
- {row.label}
431
- </span>
432
- </button>
433
- </TooltipTrigger>
434
- <TooltipContent side="right" align="center" className="font-mono text-[11px]">
435
- {row.tooltip}
436
- </TooltipContent>
437
- </Tooltip>
438
- );
439
- });
@@ -1,127 +0,0 @@
1
- 'use client';
2
-
3
- import { Loader2, Send, Terminal, WifiOff } from 'lucide-react';
4
- import { useMemo } from 'react';
5
-
6
- import { CopyButton } from '@djangocfg/ui-core/components';
7
-
8
- import JsonTree from '../../../JsonTree';
9
- import { usePlaygroundContext } from '../../context/PlaygroundContext';
10
- import { EmptyState, ScrollArea, StatusBadge } from './ui';
11
-
12
- // ─── JsonTree config (static, no re-creation on render) ──────────────────────
13
-
14
- const JSON_TREE_CONFIG = {
15
- maxAutoExpandDepth: 2,
16
- maxAutoExpandArrayItems: 10,
17
- maxAutoExpandObjectKeys: 5,
18
- maxStringLength: 200,
19
- collectionLimit: 50,
20
- showCollectionInfo: true,
21
- showExpandControls: true,
22
- showActionButtons: false,
23
- preserveKeyOrder: true,
24
- className: 'border-0 rounded-none',
25
- } as const;
26
-
27
- // ─── ResponsePanel ────────────────────────────────────────────────────────────
28
-
29
- export function ResponsePanel() {
30
- const { state } = usePlaygroundContext();
31
- const { response, loading, selectedEndpoint } = state;
32
-
33
- // ── Normalise response data ───────────────────────────────────────────────
34
- // Always try to parse as JSON first so JsonTree gets an object, not a string.
35
- // Falls back to raw text for non-JSON responses (HTML errors, plain text, etc.)
36
- const { treeData, rawText } = useMemo(() => {
37
- const d = response?.data;
38
- if (d == null) return { treeData: null, rawText: '' };
39
-
40
- if (typeof d === 'string') {
41
- try {
42
- return { treeData: JSON.parse(d), rawText: d };
43
- } catch {
44
- return { treeData: null, rawText: d };
45
- }
46
- }
47
-
48
- return { treeData: d, rawText: JSON.stringify(d, null, 2) };
49
- }, [response?.data]);
50
-
51
- // ── Derived ───────────────────────────────────────────────────────────────
52
- const sizeKb = rawText ? `${(rawText.length / 1024).toFixed(1)} KB` : '';
53
- const duration = response?.duration != null ? `${response.duration}ms` : '';
54
- const hasError = Boolean(response?.error);
55
- const hasStatus = response?.status != null;
56
- const hasCopy = Boolean(rawText);
57
-
58
- // ── Early returns ─────────────────────────────────────────────────────────
59
- if (loading) {
60
- return (
61
- <div className="flex items-center justify-center h-full gap-2">
62
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
63
- <span className="text-xs text-muted-foreground">Sending…</span>
64
- </div>
65
- );
66
- }
67
-
68
- if (!selectedEndpoint) return <EmptyState icon={Terminal} text="Response will appear here" />;
69
- if (!response) return <EmptyState icon={Send} text='Press "Send Request" to see the response' />;
70
-
71
- // Pure network error (no HTTP response at all — CORS, offline, timeout)
72
- if (hasError && !hasStatus) {
73
- return (
74
- <EmptyState
75
- icon={WifiOff}
76
- text={response.error!}
77
- className="text-destructive [&_svg]:text-destructive"
78
- />
79
- );
80
- }
81
-
82
- // ── Render ────────────────────────────────────────────────────────────────
83
- return (
84
- <>
85
- {/* Status bar */}
86
- <div className="shrink-0 border-b px-4 py-2 flex items-center justify-between gap-3 bg-muted/20">
87
- <div className="flex items-center gap-2 min-w-0">
88
- {hasStatus && <StatusBadge status={response.status!} />}
89
- {response.statusText && (
90
- <span className="text-xs text-muted-foreground truncate">{response.statusText}</span>
91
- )}
92
- {sizeKb && (
93
- <span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{sizeKb}</span>
94
- )}
95
- {duration && (
96
- <span className="text-[10px] text-muted-foreground/50 tabular-nums shrink-0">{duration}</span>
97
- )}
98
- </div>
99
- {hasCopy && (
100
- <CopyButton value={rawText} variant="ghost" size="sm" className="h-6 px-2 text-[10px] text-muted-foreground shrink-0">
101
- Copy
102
- </CopyButton>
103
- )}
104
- </div>
105
-
106
- {/* HTTP-level error body (4xx/5xx — has status but also error flag) */}
107
- {hasError && (
108
- <div className="shrink-0 mx-4 mt-3 rounded border border-destructive/20 bg-destructive/5 px-3 py-2">
109
- <p className="text-xs text-destructive">{response.error}</p>
110
- </div>
111
- )}
112
-
113
- {/* Body */}
114
- <ScrollArea>
115
- {treeData != null ? (
116
- <JsonTree title="Response Body" data={treeData} config={JSON_TREE_CONFIG} />
117
- ) : rawText ? (
118
- <pre className="p-4 text-[11px] font-mono text-foreground/70 whitespace-pre-wrap break-all leading-relaxed">
119
- {rawText}
120
- </pre>
121
- ) : (
122
- <div className="py-10 text-center text-xs text-muted-foreground">Empty response body</div>
123
- )}
124
- </ScrollArea>
125
- </>
126
- );
127
- }