radiant-docs 0.1.64 → 0.1.65

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 (28) hide show
  1. package/package.json +4 -3
  2. package/template/package-lock.json +1991 -4096
  3. package/template/package.json +7 -19
  4. package/template/src/components/OpenApiPage.astro +107 -18
  5. package/template/src/components/Sidebar.astro +1 -0
  6. package/template/src/components/endpoint/PlaygroundBar.astro +1 -1
  7. package/template/src/components/endpoint/PlaygroundField.astro +433 -71
  8. package/template/src/components/endpoint/PlaygroundForm.astro +470 -87
  9. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  10. package/template/src/components/endpoint/ResponseDisplay.astro +109 -2
  11. package/template/src/components/endpoint/ResponseFieldTree.astro +1 -1
  12. package/template/src/components/endpoint/ResponseFields.astro +97 -28
  13. package/template/src/components/endpoint/ResponseSnippets.astro +19 -7
  14. package/template/src/layouts/Layout.astro +94 -0
  15. package/template/src/lib/openapi/operation-doc.ts +308 -70
  16. package/template/src/lib/openapi/response-content.ts +133 -0
  17. package/template/src/lib/openapi/schema-variant-labels.ts +213 -0
  18. package/template/.vscode/extensions.json +0 -4
  19. package/template/.vscode/launch.json +0 -11
  20. package/template/scripts/generate-og-images.mjs +0 -667
  21. package/template/scripts/generate-og-metadata.mjs +0 -206
  22. package/template/scripts/generate-proxy-allowed-origins.mjs +0 -48
  23. package/template/scripts/generate-robots-txt.mjs +0 -47
  24. package/template/scripts/publish-shiki-platform-assets.mjs +0 -1177
  25. package/template/scripts/remove-assistant-for-non-pro.mjs +0 -28
  26. package/template/scripts/stamp-image-versions.mjs +0 -355
  27. package/template/scripts/stamp-og-image-versions.mjs +0 -199
  28. package/template/scripts/stamp-pagefind-runtime-version.mjs +0 -140
@@ -10,7 +10,7 @@ import {
10
10
  type OpenApiRequestFields,
11
11
  type OpenApiRequestSectionVariantData,
12
12
  } from "../../lib/openapi/operation-doc";
13
- import Accordion from "../user/Accordion.astro";
13
+ import ListChevronsToggle from "../ui/ListChevronsToggle.astro";
14
14
  import PlaygroundBar from "./PlaygroundBar.astro";
15
15
  import ResponseDisplay from "./ResponseDisplay.astro";
16
16
  import PlaygroundField from "./PlaygroundField.astro";
@@ -27,6 +27,7 @@ interface Props {
27
27
  >;
28
28
  bodyDescription?: string;
29
29
  bodyDefaultKind?: "object" | "array";
30
+ requestBodyContentType?: string;
30
31
  }
31
32
 
32
33
  const {
@@ -36,6 +37,7 @@ const {
36
37
  requestSectionVariants = {},
37
38
  bodyDescription = "",
38
39
  bodyDefaultKind,
40
+ requestBodyContentType,
39
41
  } = Astro.props;
40
42
  const headers: Record<string, string> = OPENAPI_REQUEST_SECTION_LABELS;
41
43
  const config = await getConfig();
@@ -70,6 +72,11 @@ const sectionVariantFieldNames = Object.fromEntries(
70
72
  ),
71
73
  ]),
72
74
  );
75
+ const headerValuePrefixes = Object.fromEntries(
76
+ requestFields.header
77
+ .filter((field) => field.valuePrefix)
78
+ .map((field) => [field.name.toLowerCase(), field.valuePrefix]),
79
+ );
73
80
  ---
74
81
 
75
82
  <div
@@ -77,8 +84,10 @@ const sectionVariantFieldNames = Object.fromEntries(
77
84
  loading: false,
78
85
  response: null,
79
86
  queryFieldMeta: ${JSON.stringify(queryFieldMeta)},
87
+ headerValuePrefixes: ${JSON.stringify(headerValuePrefixes)},
80
88
  sectionVariantFieldNames: ${JSON.stringify(sectionVariantFieldNames)},
81
89
  bodyDefaultKind: ${JSON.stringify(bodyDefaultKind ?? null)},
90
+ requestBodyContentType: ${JSON.stringify(requestBodyContentType ?? null)},
82
91
  shiki: ${JSON.stringify(shikiRuntimeConfig ?? null)},
83
92
  selectedSectionVariants: {},
84
93
  inputs: {
@@ -88,6 +97,9 @@ const sectionVariantFieldNames = Object.fromEntries(
88
97
  cookie: {},
89
98
  body: {}
90
99
  },
100
+ destroy() {
101
+ this.revokeResponseObjectUrl();
102
+ },
91
103
  getSectionVariantFieldNames(section, variantIndex) {
92
104
  const sectionVariants = this.sectionVariantFieldNames[section] || [];
93
105
  const variantNames = sectionVariants[variantIndex];
@@ -179,6 +191,10 @@ const sectionVariantFieldNames = Object.fromEntries(
179
191
  return undefined;
180
192
  }
181
193
 
194
+ if (this.isFileValue(value)) {
195
+ return value;
196
+ }
197
+
182
198
  if (Array.isArray(value)) {
183
199
  const cleaned = value
184
200
  .map((item) => this.sanitizeBodyValue(item))
@@ -201,8 +217,217 @@ const sectionVariantFieldNames = Object.fromEntries(
201
217
  return (
202
218
  value !== null &&
203
219
  typeof value === 'object' &&
204
- !Array.isArray(value)
220
+ !Array.isArray(value) &&
221
+ !this.isFileValue(value)
222
+ );
223
+ },
224
+ isFileValue(value) {
225
+ return (
226
+ (typeof File !== 'undefined' && value instanceof File) ||
227
+ (typeof Blob !== 'undefined' && value instanceof Blob)
228
+ );
229
+ },
230
+ revokeResponseObjectUrl() {
231
+ const objectUrl = this.response?.bodyObjectUrl;
232
+ if (objectUrl && typeof URL !== 'undefined') {
233
+ URL.revokeObjectURL(objectUrl);
234
+ }
235
+ },
236
+ getBaseContentType(contentType) {
237
+ return String(contentType || '').split(';')[0].trim().toLowerCase();
238
+ },
239
+ getMediaKind(contentType) {
240
+ const baseContentType = this.getBaseContentType(contentType);
241
+ if (baseContentType.startsWith('audio/')) return 'audio';
242
+ if (baseContentType.startsWith('image/')) return 'image';
243
+ if (baseContentType.startsWith('video/')) return 'video';
244
+ return null;
245
+ },
246
+ getTextPreviewLanguage(contentType, filename) {
247
+ const baseContentType = this.getBaseContentType(contentType);
248
+ const normalizedFilename = String(filename || '').toLowerCase();
249
+ if (baseContentType === 'text/event-stream') return 'yaml';
250
+ if (
251
+ baseContentType === 'application/json' ||
252
+ baseContentType.endsWith('+json') ||
253
+ normalizedFilename.endsWith('.json') ||
254
+ normalizedFilename.endsWith('.jsonl')
255
+ ) {
256
+ return 'json';
257
+ }
258
+ if (
259
+ baseContentType === 'application/yaml' ||
260
+ baseContentType === 'application/x-yaml' ||
261
+ normalizedFilename.endsWith('.yaml') ||
262
+ normalizedFilename.endsWith('.yml')
263
+ ) {
264
+ return 'yaml';
265
+ }
266
+ return 'plaintext';
267
+ },
268
+ isJsonContentType(contentType) {
269
+ const baseContentType = this.getBaseContentType(contentType);
270
+ return baseContentType === 'application/json' || baseContentType.endsWith('+json');
271
+ },
272
+ isTextLikeFilename(filename) {
273
+ const normalizedFilename = String(filename || '').toLowerCase();
274
+ return [
275
+ '.csv',
276
+ '.css',
277
+ '.html',
278
+ '.js',
279
+ '.json',
280
+ '.jsonl',
281
+ '.md',
282
+ '.mjs',
283
+ '.svg',
284
+ '.ts',
285
+ '.txt',
286
+ '.xml',
287
+ '.yaml',
288
+ '.yml',
289
+ ].some((extension) => normalizedFilename.endsWith(extension));
290
+ },
291
+ isTextPreviewContentType(contentType, filename) {
292
+ const baseContentType = this.getBaseContentType(contentType);
293
+ if (!baseContentType) return true;
294
+ if (baseContentType.startsWith('text/')) return true;
295
+ if (this.isJsonContentType(baseContentType)) return true;
296
+ if (baseContentType === 'application/x-ndjson') return true;
297
+ if (baseContentType === 'application/xml' || baseContentType.endsWith('+xml')) {
298
+ return true;
299
+ }
300
+ if (
301
+ baseContentType === 'application/javascript' ||
302
+ baseContentType === 'application/x-javascript'
303
+ ) {
304
+ return true;
305
+ }
306
+ return this.isTextLikeFilename(filename);
307
+ },
308
+ getFilenameFromContentDisposition(contentDisposition) {
309
+ const parts = String(contentDisposition || '')
310
+ .split(';')
311
+ .map((part) => part.trim())
312
+ .filter(Boolean);
313
+ const filenameStarPart = parts.find((part) =>
314
+ part.toLowerCase().startsWith('filename*='),
205
315
  );
316
+ const filenamePart =
317
+ filenameStarPart ||
318
+ parts.find((part) => part.toLowerCase().startsWith('filename='));
319
+
320
+ if (!filenamePart) return '';
321
+
322
+ let value = filenamePart.slice(filenamePart.indexOf('=') + 1).trim();
323
+ if (
324
+ (value.startsWith('"') && value.endsWith('"')) ||
325
+ (value.startsWith("'") && value.endsWith("'"))
326
+ ) {
327
+ value = value.slice(1, -1);
328
+ }
329
+
330
+ if (filenameStarPart && value.toLowerCase().startsWith("utf-8''")) {
331
+ value = value.slice(7);
332
+ }
333
+
334
+ try {
335
+ value = decodeURIComponent(value);
336
+ } catch (_error) {
337
+ // Leave non-percent-encoded filenames as-is.
338
+ }
339
+
340
+ return value.split(String.fromCharCode(92)).join('/').split('/').pop() || '';
341
+ },
342
+ getResponseFileExtension(contentType) {
343
+ const baseContentType = this.getBaseContentType(contentType);
344
+ const extensionByType = {
345
+ 'application/gzip': '.gz',
346
+ 'application/json': '.json',
347
+ 'application/octet-stream': '.bin',
348
+ 'application/pdf': '.pdf',
349
+ 'application/zip': '.zip',
350
+ 'audio/aac': '.aac',
351
+ 'audio/flac': '.flac',
352
+ 'audio/mpeg': '.mp3',
353
+ 'audio/ogg': '.ogg',
354
+ 'audio/wav': '.wav',
355
+ 'audio/webm': '.webm',
356
+ 'image/gif': '.gif',
357
+ 'image/jpeg': '.jpg',
358
+ 'image/png': '.png',
359
+ 'image/svg+xml': '.svg',
360
+ 'image/webp': '.webp',
361
+ 'text/csv': '.csv',
362
+ 'text/plain': '.txt',
363
+ 'video/mp4': '.mp4',
364
+ 'video/mpeg': '.mpeg',
365
+ 'video/quicktime': '.mov',
366
+ 'video/webm': '.webm',
367
+ };
368
+ return extensionByType[baseContentType] || '';
369
+ },
370
+ getResponseFilename(headers, contentType) {
371
+ const disposition =
372
+ headers['content-disposition'] || headers['Content-Disposition'] || '';
373
+ const filename = this.getFilenameFromContentDisposition(disposition);
374
+ if (filename) return filename;
375
+ return 'response' + this.getResponseFileExtension(contentType);
376
+ },
377
+ formatByteSize(bytes) {
378
+ if (!Number.isFinite(bytes)) return '';
379
+ if (bytes < 1024) return bytes + ' B';
380
+ const units = ['KB', 'MB', 'GB'];
381
+ let size = bytes / 1024;
382
+ let unitIndex = 0;
383
+ while (size >= 1024 && unitIndex < units.length - 1) {
384
+ size = size / 1024;
385
+ unitIndex += 1;
386
+ }
387
+ return size.toFixed(size >= 10 ? 1 : 2) + ' ' + units[unitIndex];
388
+ },
389
+ buildFormData(rawValue) {
390
+ const cleanedValue = this.sanitizeBodyValue(rawValue);
391
+ if (cleanedValue === undefined) return undefined;
392
+
393
+ const formData = new FormData();
394
+ let hasEntries = false;
395
+
396
+ const appendValue = (key, value) => {
397
+ const cleaned = this.sanitizeBodyValue(value);
398
+ if (cleaned === undefined) return;
399
+
400
+ if (this.isFileValue(cleaned)) {
401
+ formData.append(key, cleaned);
402
+ hasEntries = true;
403
+ return;
404
+ }
405
+
406
+ if (Array.isArray(cleaned)) {
407
+ cleaned.forEach((item) => appendValue(key, item));
408
+ return;
409
+ }
410
+
411
+ if (this.isPlainObject(cleaned)) {
412
+ Object.entries(cleaned).forEach(([nestedKey, nestedValue]) => {
413
+ appendValue(key + '[' + nestedKey + ']', nestedValue);
414
+ });
415
+ return;
416
+ }
417
+
418
+ formData.append(key, String(cleaned));
419
+ hasEntries = true;
420
+ };
421
+
422
+ if (this.isPlainObject(cleanedValue)) {
423
+ Object.entries(cleanedValue).forEach(([key, value]) => {
424
+ appendValue(key, value);
425
+ });
426
+ } else {
427
+ appendValue('body', cleanedValue);
428
+ }
429
+
430
+ return hasEntries ? formData : undefined;
206
431
  },
207
432
  serializeQueryObject(params, key, rawValue, meta = {}) {
208
433
  const cleanedObject = this.sanitizeBodyValue(rawValue);
@@ -334,6 +559,37 @@ const sectionVariantFieldNames = Object.fromEntries(
334
559
 
335
560
  return cookieParts.join('; ');
336
561
  },
562
+ formatHeaderValue(name, value) {
563
+ const valueText =
564
+ value === undefined || value === null ? '' : String(value).trim();
565
+ if (!valueText) return '';
566
+
567
+ const prefix = this.headerValuePrefixes[String(name).toLowerCase()];
568
+ if (!prefix) return valueText;
569
+
570
+ const prefixText = String(prefix).trim();
571
+ if (!prefixText) return valueText;
572
+
573
+ return valueText.toLowerCase().startsWith(prefixText.toLowerCase() + ' ')
574
+ ? valueText
575
+ : prefixText + ' ' + valueText;
576
+ },
577
+ buildRequestHeaders() {
578
+ const headers = {};
579
+ const contentType = String(this.requestBodyContentType || '').toLowerCase();
580
+ if (!contentType.includes('multipart/form-data')) {
581
+ headers['Content-Type'] = this.requestBodyContentType || 'application/json';
582
+ }
583
+
584
+ Object.entries(this.inputs.header || {}).forEach(([key, value]) => {
585
+ const headerName = String(key).trim();
586
+ const headerValue = this.formatHeaderValue(headerName, value);
587
+ if (!headerName || !headerValue) return;
588
+ headers[headerName] = headerValue;
589
+ });
590
+
591
+ return headers;
592
+ },
337
593
  buildQueryParams() {
338
594
  const params = new URLSearchParams();
339
595
 
@@ -420,6 +676,7 @@ const sectionVariantFieldNames = Object.fromEntries(
420
676
  async sendRequest(event) {
421
677
  if (!this.validateInputs(event?.currentTarget)) return;
422
678
 
679
+ this.revokeResponseObjectUrl();
423
680
  this.loading = true;
424
681
  try {
425
682
  // 1. Construct URL with Path Parameters
@@ -433,22 +690,30 @@ const sectionVariantFieldNames = Object.fromEntries(
433
690
  // 3. Prepare Fetch Options
434
691
  const options = {
435
692
  method: ${JSON.stringify(route.openApiMethod.toUpperCase())},
436
- headers: {
437
- 'Content-Type': 'application/json',
438
- ...this.inputs.header
439
- }
693
+ headers: this.buildRequestHeaders()
440
694
  };
441
695
  const cookieHeader = this.buildCookieHeader();
442
696
 
443
697
  // 4. Add Body if needed
444
698
  if (['POST', 'PUT', 'PATCH'].includes(options.method)) {
445
- const sanitizedBody = this.sanitizeBodyValue(this.inputs.body);
446
- if (sanitizedBody !== undefined) {
447
- options.body = JSON.stringify(sanitizedBody);
699
+ const isMultipartBody = String(this.requestBodyContentType || '')
700
+ .toLowerCase()
701
+ .includes('multipart/form-data');
702
+
703
+ if (isMultipartBody) {
704
+ const formData = this.buildFormData(this.inputs.body);
705
+ if (formData !== undefined) {
706
+ options.body = formData;
707
+ }
448
708
  } else {
449
- const defaultBody = this.getDefaultRequestBody();
450
- if (defaultBody !== undefined) {
451
- options.body = JSON.stringify(defaultBody);
709
+ const sanitizedBody = this.sanitizeBodyValue(this.inputs.body);
710
+ if (sanitizedBody !== undefined) {
711
+ options.body = JSON.stringify(sanitizedBody);
712
+ } else {
713
+ const defaultBody = this.getDefaultRequestBody();
714
+ if (defaultBody !== undefined) {
715
+ options.body = JSON.stringify(defaultBody);
716
+ }
452
717
  }
453
718
  }
454
719
  }
@@ -498,9 +763,54 @@ const sectionVariantFieldNames = Object.fromEntries(
498
763
  this.shiki,
499
764
  );
500
765
  const contentType = res.headers.get('content-type') || '';
766
+ const responseFilename = this.getResponseFilename(responseHeaders, contentType);
767
+ const mediaKind = this.getMediaKind(contentType);
768
+ const shouldPreviewAsText = this.isTextPreviewContentType(
769
+ contentType,
770
+ responseFilename,
771
+ );
772
+
773
+ if (mediaKind) {
774
+ const blob = await res.blob();
775
+ const bodyObjectUrl = URL.createObjectURL(blob);
776
+ this.response = {
777
+ status,
778
+ headers: responseHeaders,
779
+ bodyKind: 'media',
780
+ mediaKind,
781
+ bodyObjectUrl,
782
+ bodyContentType: contentType,
783
+ bodyFilename: responseFilename,
784
+ bodySize: blob.size,
785
+ bodySizeLabel: this.formatByteSize(blob.size),
786
+ highlightedHeaders
787
+ };
788
+ return;
789
+ }
790
+
791
+ if (!shouldPreviewAsText) {
792
+ const blob = await res.blob();
793
+ const bodyObjectUrl = URL.createObjectURL(blob);
794
+ this.response = {
795
+ status,
796
+ headers: responseHeaders,
797
+ bodyKind: 'binary',
798
+ bodyObjectUrl,
799
+ bodyContentType: contentType,
800
+ bodyFilename: responseFilename,
801
+ bodySize: blob.size,
802
+ bodySizeLabel: this.formatByteSize(blob.size),
803
+ highlightedHeaders
804
+ };
805
+ return;
806
+ }
807
+
501
808
  const responseText = await res.text();
502
809
 
503
- if (contentType.includes('application/json')) {
810
+ if (contentType.toLowerCase().includes('text/event-stream')) {
811
+ responseDataText = responseText || '(empty response)';
812
+ responseDataLanguage = 'yaml';
813
+ } else if (this.isJsonContentType(contentType)) {
504
814
  try {
505
815
  const parsed =
506
816
  responseText.trim().length > 0 ? JSON.parse(responseText) : null;
@@ -508,9 +818,17 @@ const sectionVariantFieldNames = Object.fromEntries(
508
818
  responseDataLanguage = 'json';
509
819
  } catch (parseError) {
510
820
  responseDataText = responseText || '(empty response)';
821
+ responseDataLanguage = this.getTextPreviewLanguage(
822
+ contentType,
823
+ responseFilename,
824
+ );
511
825
  }
512
826
  } else {
513
827
  responseDataText = responseText || '(empty response)';
828
+ responseDataLanguage = this.getTextPreviewLanguage(
829
+ contentType,
830
+ responseFilename,
831
+ );
514
832
  }
515
833
  const highlightedData = await window.RadiantPlaygroundHighlightCode(
516
834
  responseDataText,
@@ -521,6 +839,9 @@ const sectionVariantFieldNames = Object.fromEntries(
521
839
  this.response = {
522
840
  status,
523
841
  headers: responseHeaders,
842
+ bodyKind: 'code',
843
+ bodyContentType: contentType,
844
+ bodyFilename: responseFilename,
524
845
  highlightedData,
525
846
  highlightedHeaders
526
847
  };
@@ -538,7 +859,7 @@ const sectionVariantFieldNames = Object.fromEntries(
538
859
  data-playground-form-root
539
860
  class="flex w-full h-full min-h-0 flex-col"
540
861
  >
541
-
862
+ <div>
542
863
  <PlaygroundBar route={route} serverUrl={serverUrl}>
543
864
  <button
544
865
  @click="sendRequest($event)"
@@ -561,15 +882,14 @@ const sectionVariantFieldNames = Object.fromEntries(
561
882
  </span>
562
883
  </button>
563
884
  </PlaygroundBar>
564
-
885
+ </div>
565
886
 
566
887
  <div class="min-h-0 flex-1 flex flex-col lg:flex-row-reverse">
567
888
  <div class="lg:flex-1 w-full min-h-0 h-[calc(40dvh-2rem-23px-12px)] max-h-[calc(40dvh-2rem-23px-12px)] lg:h-auto lg:max-h-[calc(100dvh-4rem-46px-24px)] overflow-hidden pt-4 lg:pb-6">
568
889
  <ResponseDisplay />
569
890
  </div>
570
891
  <div class="lg:flex-1 relative">
571
- <div class="flex-3 min-h-0 max-h-[calc(60dvh-2rem-23px-12px)] lg:max-h-[calc(100dvh-4rem-46px-24px)] overflow-y-auto overscroll-contain [scrollbar-width:thin] [scrollbar-color:var(--color-neutral-300)_transparent] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-neutral-300/70 hover:[&::-webkit-scrollbar-thumb]:bg-neutral-300/90 dark:[scrollbar-color:var(--color-neutral-700)_transparent] dark:[&::-webkit-scrollbar-thumb]:bg-neutral-700/70 dark:hover:[&::-webkit-scrollbar-thumb]:bg-neutral-700/90">
572
- <div class="pointer-events-none sticky top-0 z-10 -mb-4 h-4 bg-linear-to-b from-background via-white/60 to-transparent dark:via-neutral-900/70"></div>
892
+ <div class="flex-3 min-h-0 max-h-[calc(60dvh-2rem-23px-12px)] lg:max-h-[calc(100dvh-4rem-46px-24px)] overflow-y-auto overscroll-contain mask-[linear-gradient(to_bottom,transparent_0,black_16px,black_100%)] [-webkit-mask-image:linear-gradient(to_bottom,transparent_0,black_16px,black_100%)] [scrollbar-width:thin] [scrollbar-color:var(--color-neutral-300)_transparent] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-neutral-300/70 hover:[&::-webkit-scrollbar-thumb]:bg-neutral-300/90 dark:[scrollbar-color:var(--color-neutral-700)_transparent] dark:[&::-webkit-scrollbar-thumb]:bg-neutral-700/70 dark:hover:[&::-webkit-scrollbar-thumb]:bg-neutral-700/90">
573
893
  <div class="space-y-4 lg:pr-4 pt-4 pb-6">
574
894
  {
575
895
  Object.keys(requestFields).map(
@@ -578,97 +898,160 @@ const sectionVariantFieldNames = Object.fromEntries(
578
898
  const sectionVariantData =
579
899
  requestSectionVariants[key as keyof RequestFields];
580
900
  const sectionVariants = sectionVariantData?.variants || [];
901
+ const sectionVariantCount = sectionVariants.reduce(
902
+ (sum, variant) => sum + variant.fields.length,
903
+ 0,
904
+ );
905
+ const hasCommonFields = sectionFields.length > 0;
906
+ const hasVariants = sectionVariantCount > 0;
581
907
  const keyLiteral = JSON.stringify(key);
582
908
 
583
- if (sectionFields.length === 0 && sectionVariants.length === 0) {
909
+ if (!hasCommonFields && !hasVariants) {
584
910
  return null;
585
911
  }
586
912
 
913
+ const noun =
914
+ key === "body"
915
+ ? sectionFields.length === 1
916
+ ? "field"
917
+ : "fields"
918
+ : sectionFields.length === 1
919
+ ? "parameter"
920
+ : "parameters";
921
+ const variantNoun = key === "body" ? "field" : "parameter";
922
+
587
923
  return (
588
- <div class="border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] rounded-xl p-4 pb-0 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)] [&_[role='region']]:border-b-0">
589
- <Accordion title={headers[key]} defaultOpen titleSize="xl">
590
- {key === "body" && formattedBodyDescription && (
591
- <div
592
- class="mb-4 prose-rules prose-sm! text-neutral-500 **:text-neutral-500 dark:text-neutral-400 dark:**:text-neutral-400"
593
- set:html={formattedBodyDescription}
594
- />
595
- )}
596
- {sectionFields.map((field) => (
597
- <div class="border-b border-b-neutral-100 dark:border-b-neutral-800 last:border-none pb-4 mb-4 last:pb-0 last:mb-0 first:pt-2">
598
- <PlaygroundField field={field} requestPart={key} />
924
+ <section>
925
+ <h4 class="rd-document-heading text-base font-semibold text-neutral-900 dark:text-neutral-100">
926
+ {headers[key]}
927
+ </h4>
928
+ {key === "body" && formattedBodyDescription && (
929
+ <div
930
+ class="mt-2 prose-rules prose-sm! text-neutral-500 **:text-neutral-500 dark:text-neutral-400 dark:**:text-neutral-400"
931
+ set:html={formattedBodyDescription}
932
+ />
933
+ )}
934
+ {hasCommonFields && (
935
+ <div x-data="{ expanded: true }" class="mt-3">
936
+ <div class="w-full overflow-hidden rounded-xl border-[0.5px] border-neutral-900/12 bg-neutral-50/50 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] transition-colors duration-200 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]">
937
+ <button
938
+ type="button"
939
+ x-on:click="expanded = !expanded"
940
+ x-bind:aria-expanded="expanded"
941
+ class="group flex w-full cursor-pointer items-center justify-between gap-3 px-4 py-2.5 text-left text-sm font-medium text-neutral-700 transition-colors duration-200 hover:bg-neutral-50 dark:text-neutral-300 dark:hover:bg-neutral-800/60"
942
+ >
943
+ <span class="inline-flex items-center gap-2">
944
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 transition duration-200 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
945
+ <span>
946
+ {sectionFields.length} {noun}
947
+ </span>
948
+ </span>
949
+ </button>
950
+ <div x-show="expanded" x-collapse x-cloak>
951
+ <div class="border-t border-neutral-100 p-4 dark:border-neutral-800">
952
+ {sectionFields.map((field) => (
953
+ <div class="border-b border-b-neutral-100 pb-4 mb-4 last:border-none last:pb-0 last:mb-0 dark:border-b-neutral-800">
954
+ <PlaygroundField field={field} requestPart={key} />
955
+ </div>
956
+ ))}
957
+ </div>
958
+ </div>
599
959
  </div>
600
- ))}
601
- {sectionVariants.length > 0 && (
602
- <div class:list={["space-y-2", sectionFields.length > 0 && "pt-1"]}>
603
- {sectionVariantData?.variantType === "oneOf" ? (
604
- <>
605
- <p class="text-xs text-neutral-500 dark:text-neutral-400">
606
- Select one variant.
607
- </p>
608
- <div class="inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-900/60">
609
- {sectionVariants.map((variant, variantIndex) => (
960
+ </div>
961
+ )}
962
+ {hasVariants && (
963
+ <div
964
+ class:list={[
965
+ "space-y-2",
966
+ hasCommonFields ? "mt-3" : "mt-4",
967
+ ]}
968
+ >
969
+ <p class="text-xs text-neutral-500 dark:text-neutral-400">
970
+ {sectionVariantData?.variantType === "anyOf"
971
+ ? "One or more variants may apply."
972
+ : "One of these variants applies."}
973
+ </p>
974
+ {sectionVariantData?.variantType === "oneOf" && (
975
+ <div class="inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg bg-neutral-100 p-1 dark:bg-neutral-900/60">
976
+ {sectionVariants.map((variant, variantIndex) => (
977
+ <button
978
+ type="button"
979
+ @click={`selectSectionVariant(${keyLiteral}, ${variantIndex})`}
980
+ :class={`{
981
+ 'bg-white text-neutral-900 shadow-xs ring-1 ring-neutral-200 dark:bg-(--rd-code-surface) dark:text-neutral-100 dark:ring-neutral-700/70': getSelectedSectionVariant(${keyLiteral}) === ${variantIndex},
982
+ 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/70 dark:hover:text-neutral-200': getSelectedSectionVariant(${keyLiteral}) !== ${variantIndex}
983
+ }`}
984
+ class="inline-flex h-7 cursor-pointer items-center justify-center whitespace-nowrap rounded-md px-2.5 text-[11px] font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-300 dark:focus-visible:ring-neutral-700"
985
+ >
986
+ {variant.label}
987
+ </button>
988
+ ))}
989
+ </div>
990
+ )}
991
+ {sectionVariants.map((variant, variantIndex) => (
992
+ <>
993
+ <div
994
+ class="rounded-lg border border-neutral-200 bg-white p-3 dark:border-neutral-800 dark:bg-neutral-900/60"
995
+ x-show={
996
+ sectionVariantData?.variantType === "oneOf"
997
+ ? `getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`
998
+ : undefined
999
+ }
1000
+ x-cloak={sectionVariantData?.variantType === "oneOf"}
1001
+ >
1002
+ <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
1003
+ {variant.label}
1004
+ </div>
1005
+ <div x-data="{ expanded: true }">
1006
+ <div class="w-full overflow-hidden rounded-lg border-[0.5px] border-neutral-900/8 bg-white shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-6px_rgba(0,0,0,0.08)] transition-colors duration-200 dark:border-white/6 dark:bg-(--rd-code-surface) dark:shadow-[0_-.5px_1px_rgba(255,255,255,0.15),0_5px_12px_-6px_rgba(0,0,0,0.2)]">
610
1007
  <button
611
1008
  type="button"
612
- @click={`selectSectionVariant(${keyLiteral}, ${variantIndex})`}
613
- :class={`{
614
- 'bg-white text-neutral-900 shadow-xs ring-1 ring-neutral-200 dark:bg-(--rd-code-surface) dark:text-neutral-100 dark:ring-neutral-700/70': getSelectedSectionVariant(${keyLiteral}) === ${variantIndex},
615
- 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-800/70 dark:hover:text-neutral-200': getSelectedSectionVariant(${keyLiteral}) !== ${variantIndex}
616
- }`}
617
- class="inline-flex h-7 items-center justify-center whitespace-nowrap rounded-md px-2.5 text-[11px] font-medium transition-all duration-200 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-300 dark:focus-visible:ring-neutral-700"
1009
+ x-on:click="expanded = !expanded"
1010
+ x-bind:aria-expanded="expanded"
1011
+ class="group flex w-full cursor-pointer items-center justify-between gap-3 px-3 py-2 text-left text-xs font-medium text-neutral-700 transition-colors duration-200 hover:bg-neutral-50 dark:text-neutral-300 dark:hover:bg-neutral-800/60"
618
1012
  >
619
- {variant.label}
1013
+ <span class="inline-flex items-center gap-2">
1014
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 transition duration-200 group-hover:text-neutral-600 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
1015
+ <span>
1016
+ {variant.fields.length}{" "}
1017
+ {variant.fields.length === 1
1018
+ ? variantNoun
1019
+ : `${variantNoun}s`}
1020
+ </span>
1021
+ </span>
620
1022
  </button>
621
- ))}
622
- </div>
623
- {sectionVariants.map((variant, variantIndex) => (
624
- <div
625
- x-show={`getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`}
626
- x-cloak
627
- >
628
- <div class="rounded-md border border-neutral-200 bg-white p-2.5 dark:border-neutral-800 dark:bg-neutral-900/60">
629
- <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
630
- {variant.label}
631
- </div>
632
- <div class="space-y-3">
1023
+ <div x-show="expanded" x-collapse x-cloak>
1024
+ <div class="border-t border-neutral-100 px-3 py-3 dark:border-neutral-800">
633
1025
  {variant.fields.map((field) => (
634
- <div class="border-b border-b-neutral-100 dark:border-b-neutral-800 last:border-none pb-3 last:pb-0">
1026
+ <div class="border-b border-b-neutral-100 pb-3 mb-3 last:border-none last:pb-0 last:mb-0 dark:border-b-neutral-800">
635
1027
  <PlaygroundField
636
1028
  field={field}
637
1029
  requestPart={key}
638
- defaultsEnabledExpr={`getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`}
1030
+ defaultsEnabledExpr={
1031
+ sectionVariantData?.variantType ===
1032
+ "oneOf"
1033
+ ? `getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`
1034
+ : undefined
1035
+ }
1036
+ requiredEnabledExpr={
1037
+ sectionVariantData?.variantType ===
1038
+ "oneOf"
1039
+ ? `getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`
1040
+ : undefined
1041
+ }
639
1042
  />
640
1043
  </div>
641
1044
  ))}
642
1045
  </div>
643
1046
  </div>
644
1047
  </div>
645
- ))}
646
- </>
647
- ) : (
648
- <>
649
- <p class="text-xs text-neutral-500 dark:text-neutral-400">
650
- One or more variants may apply.
651
- </p>
652
- {sectionVariants.map((variant) => (
653
- <div class="rounded-md border border-neutral-200 bg-white p-2.5 dark:border-neutral-800 dark:bg-neutral-900/60">
654
- <div class="mb-2 text-xs font-medium text-neutral-600 dark:text-neutral-400">
655
- {variant.label}
656
- </div>
657
- <div class="space-y-3">
658
- {variant.fields.map((field) => (
659
- <div class="border-b border-b-neutral-100 dark:border-b-neutral-800 last:border-none pb-3 last:pb-0">
660
- <PlaygroundField field={field} requestPart={key} />
661
- </div>
662
- ))}
663
- </div>
664
- </div>
665
- ))}
666
- </>
667
- )}
668
- </div>
669
- )}
670
- </Accordion>
671
- </div>
1048
+ </div>
1049
+ </div>
1050
+ </>
1051
+ ))}
1052
+ </div>
1053
+ )}
1054
+ </section>
672
1055
  );
673
1056
  },
674
1057
  )