radiant-docs 0.1.63 → 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 (29) 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/client-shiki-config.ts +6 -3
  16. package/template/src/lib/openapi/operation-doc.ts +308 -70
  17. package/template/src/lib/openapi/response-content.ts +133 -0
  18. package/template/src/lib/openapi/schema-variant-labels.ts +213 -0
  19. package/template/.vscode/extensions.json +0 -4
  20. package/template/.vscode/launch.json +0 -11
  21. package/template/scripts/generate-og-images.mjs +0 -667
  22. package/template/scripts/generate-og-metadata.mjs +0 -206
  23. package/template/scripts/generate-proxy-allowed-origins.mjs +0 -48
  24. package/template/scripts/generate-robots-txt.mjs +0 -47
  25. package/template/scripts/publish-shiki-platform-assets.mjs +0 -1177
  26. package/template/scripts/remove-assistant-for-non-pro.mjs +0 -28
  27. package/template/scripts/stamp-image-versions.mjs +0 -355
  28. package/template/scripts/stamp-og-image-versions.mjs +0 -199
  29. package/template/scripts/stamp-pagefind-runtime-version.mjs +0 -140
@@ -507,7 +507,11 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
507
507
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
508
508
  >
509
509
  <div
510
- class="flex items-center justify-between gap-2"
510
+ class:list={[
511
+ "flex items-center justify-between gap-2",
512
+ !hasMultipleRequests &&
513
+ "border-b-[0.5px] border-(--rd-code-tab-edge-border)",
514
+ ]}
511
515
  >
512
516
  {
513
517
  hasMultipleRequests ? (
@@ -597,6 +601,7 @@ const hasMultipleRequests = requestSnippetItems.length > 1;
597
601
  class:list={[
598
602
  "relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)",
599
603
  hasMultipleRequests && "rounded-tl-xl",
604
+ !hasMultipleRequests && "border-t-0",
600
605
  ]}
601
606
  >
602
607
  <div
@@ -74,7 +74,7 @@ import { Icon } from "astro-icon/components";
74
74
  <div
75
75
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
76
76
  >
77
- <div class="flex items-center justify-between gap-2">
77
+ <div class="relative z-10 flex items-center justify-between gap-2">
78
78
  <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
79
79
  <div
80
80
  x-ref="tabList"
@@ -143,7 +143,7 @@ import { Icon } from "astro-icon/components";
143
143
  >
144
144
  <div x-show="selected === 'body'" class="h-full">
145
145
  <div
146
- x-show="!response || !response.highlightedData"
146
+ x-show="!response || (!response.highlightedData && !response.bodyObjectUrl)"
147
147
  class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center h-full text-foreground"
148
148
  >
149
149
  <div class="p-2 rounded-xl mb-1">
@@ -163,6 +163,113 @@ import { Icon } from "astro-icon/components";
163
163
  <pre
164
164
  class="relative m-0 min-w-full p-0 text-[13px] leading-6"><code data-rd-code-theme class="block min-w-full py-2.5 font-mono text-neutral-800 dark:text-neutral-200" x-html="response?.highlightedData" /></pre>
165
165
  </div>
166
+ <div
167
+ x-show="response?.bodyKind === 'media'"
168
+ x-cloak
169
+ class="min-h-full p-4"
170
+ >
171
+ <div class="mb-4 flex flex-wrap items-center justify-between gap-3">
172
+ <div class="min-w-0">
173
+ <div
174
+ class="truncate text-sm font-medium text-neutral-900 dark:text-neutral-100"
175
+ x-text="response?.bodyFilename || 'Response media'"
176
+ >
177
+ </div>
178
+ <div class="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-neutral-500 dark:text-neutral-400">
179
+ <span x-text="response?.bodyContentType || 'Binary response'">
180
+ </span>
181
+ <span x-show="response?.bodySizeLabel" aria-hidden="true">
182
+ /
183
+ </span>
184
+ <span x-text="response?.bodySizeLabel"></span>
185
+ </div>
186
+ </div>
187
+ <a
188
+ x-bind:href="response?.bodyObjectUrl"
189
+ x-bind:download="response?.bodyFilename || 'response'"
190
+ class="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-neutral-200 bg-white px-2.5 text-xs font-medium text-neutral-700 shadow-xs transition-colors hover:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-300 dark:hover:bg-neutral-900"
191
+ >
192
+ <Icon class="size-3.5" name="lucide:download" />
193
+ Download
194
+ </a>
195
+ </div>
196
+
197
+ <div
198
+ x-show="response?.mediaKind === 'audio'"
199
+ x-cloak
200
+ class="flex min-h-28 items-center"
201
+ >
202
+ <audio
203
+ class="w-full"
204
+ controls
205
+ preload="metadata"
206
+ x-bind:src="response?.bodyObjectUrl"
207
+ >
208
+ </audio>
209
+ </div>
210
+
211
+ <div
212
+ x-show="response?.mediaKind === 'image'"
213
+ x-cloak
214
+ class="flex min-h-48 items-start justify-center"
215
+ >
216
+ <img
217
+ class="max-h-[min(60dvh,32rem)] max-w-full rounded-lg object-contain"
218
+ x-bind:src="response?.bodyObjectUrl"
219
+ x-bind:alt="response?.bodyFilename || 'Response image'"
220
+ />
221
+ </div>
222
+
223
+ <div
224
+ x-show="response?.mediaKind === 'video'"
225
+ x-cloak
226
+ class="flex min-h-48 items-start justify-center"
227
+ >
228
+ <video
229
+ class="max-h-[min(60dvh,32rem)] max-w-full rounded-lg"
230
+ controls
231
+ preload="metadata"
232
+ x-bind:src="response?.bodyObjectUrl"
233
+ >
234
+ </video>
235
+ </div>
236
+ </div>
237
+
238
+ <div
239
+ x-show="response?.bodyKind === 'binary'"
240
+ x-cloak
241
+ class="flex min-h-full items-center justify-center p-4"
242
+ >
243
+ <div class="flex w-full max-w-md flex-col items-center text-center">
244
+ <div class="mb-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
245
+ <Icon
246
+ class="size-6 text-neutral-500 dark:text-neutral-400"
247
+ name="lucide:file-down"
248
+ />
249
+ </div>
250
+ <div
251
+ class="max-w-full truncate text-sm font-medium text-neutral-900 dark:text-neutral-100"
252
+ x-text="response?.bodyFilename || 'Binary response'"
253
+ >
254
+ </div>
255
+ <div class="mt-1 flex max-w-full flex-wrap justify-center gap-x-2 gap-y-1 text-xs text-neutral-500 dark:text-neutral-400">
256
+ <span x-text="response?.bodyContentType || 'application/octet-stream'">
257
+ </span>
258
+ <span x-show="response?.bodySizeLabel" aria-hidden="true">
259
+ /
260
+ </span>
261
+ <span x-text="response?.bodySizeLabel"></span>
262
+ </div>
263
+ <a
264
+ x-bind:href="response?.bodyObjectUrl"
265
+ x-bind:download="response?.bodyFilename || 'response'"
266
+ class="mt-4 inline-flex h-8 items-center gap-1.5 rounded-lg border border-neutral-200 bg-white px-2.5 text-xs font-medium text-neutral-700 shadow-xs transition-colors hover:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-300 dark:hover:bg-neutral-900"
267
+ >
268
+ <Icon class="size-3.5" name="lucide:download" />
269
+ Download
270
+ </a>
271
+ </div>
272
+ </div>
166
273
  </div>
167
274
 
168
275
  <div x-show="selected === 'header'" x-cloak class="h-full">
@@ -71,7 +71,7 @@ function countImmediateChildren(field: ResponseField): number {
71
71
  exclusiveMaximum={field.exclusiveMaximum}
72
72
  />
73
73
  {hasExpandableContent && (
74
- <div class="mt-2.5 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)]">
74
+ <div class="mt-2.5 w-full overflow-hidden rounded-lg border-[0.5px] border-neutral-900/8 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)]">
75
75
  <button
76
76
  type="button"
77
77
  x-on:click="expanded = !expanded"
@@ -2,6 +2,11 @@
2
2
  import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
3
3
  import ListChevronsToggle from "../ui/ListChevronsToggle.astro";
4
4
  import ResponseFieldTree from "./ResponseFieldTree.astro";
5
+ import { getSchemaVariantLabel } from "../../lib/openapi/schema-variant-labels";
6
+ import {
7
+ getBinaryResponseSummary,
8
+ type OpenApiBinaryResponseSummary,
9
+ } from "../../lib/openapi/response-content";
5
10
 
6
11
  interface Props {
7
12
  responses: OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject;
@@ -39,6 +44,7 @@ interface ResponseData {
39
44
  fields: ResponseField[];
40
45
  variants?: ResponseVariant[];
41
46
  variantType?: "oneOf" | "anyOf";
47
+ binarySummary?: OpenApiBinaryResponseSummary;
42
48
  }
43
49
 
44
50
  function getStatusCodeDotClass(statusCode: string): string {
@@ -82,7 +88,9 @@ function getSchemaEnumValues(schema: any): (string | number)[] | undefined {
82
88
  }
83
89
 
84
90
  function parseFiniteNumber(value: unknown): number | undefined {
85
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
91
+ return typeof value === "number" && Number.isFinite(value)
92
+ ? value
93
+ : undefined;
86
94
  }
87
95
 
88
96
  function parseNonNegativeInteger(value: unknown): number | undefined {
@@ -182,7 +190,10 @@ function mergeConjunctiveNumericConstraints(
182
190
  if (!second) return first;
183
191
  if (first.value > second.value) return first;
184
192
  if (second.value > first.value) return second;
185
- return { value: first.value, exclusive: first.exclusive || second.exclusive };
193
+ return {
194
+ value: first.value,
195
+ exclusive: first.exclusive || second.exclusive,
196
+ };
186
197
  };
187
198
  const pickStricterUpper = (
188
199
  first?: { value: number; exclusive: boolean },
@@ -192,7 +203,10 @@ function mergeConjunctiveNumericConstraints(
192
203
  if (!second) return first;
193
204
  if (first.value < second.value) return first;
194
205
  if (second.value < first.value) return second;
195
- return { value: first.value, exclusive: first.exclusive || second.exclusive };
206
+ return {
207
+ value: first.value,
208
+ exclusive: first.exclusive || second.exclusive,
209
+ };
196
210
  };
197
211
 
198
212
  const lower = pickStricterLower(toLowerBound(base), toLowerBound(next));
@@ -438,6 +452,23 @@ function getAdditionalPropertiesSchema(schema: any): any {
438
452
  return additionalProperties;
439
453
  }
440
454
 
455
+ function getSchemaObjectProperties(schema: any): Record<string, any> {
456
+ if (!schema || typeof schema !== "object") return {};
457
+
458
+ let properties: Record<string, any> = {};
459
+ if (schema.properties && typeof schema.properties === "object") {
460
+ properties = { ...properties, ...schema.properties };
461
+ }
462
+
463
+ if (Array.isArray(schema.allOf)) {
464
+ schema.allOf.forEach((part: any) => {
465
+ properties = { ...properties, ...getSchemaObjectProperties(part) };
466
+ });
467
+ }
468
+
469
+ return properties;
470
+ }
471
+
441
472
  function extractVariantData(
442
473
  schema: any,
443
474
  parentName: string,
@@ -458,7 +489,12 @@ function extractVariantData(
458
489
 
459
490
  const variants = variantSchemas
460
491
  .map((variantSchema: any, index: number) => ({
461
- label: `Variant ${index + 1}`,
492
+ label: getSchemaVariantLabel(variantSchema, index, {
493
+ parentSchema: schema,
494
+ siblings: variantSchemas,
495
+ getObjectProperties: getSchemaObjectProperties,
496
+ getTypeLabel: getSchemaTypeLabel,
497
+ }),
462
498
  fields: extractFieldsFromSchema(
463
499
  variantSchema,
464
500
  parentName,
@@ -568,7 +604,9 @@ function extractFieldsFromSchema(
568
604
  const nestedFields = extractFieldsFromSchema(
569
605
  basePropSchema,
570
606
  fullName,
571
- Array.isArray(basePropSchema.required) ? basePropSchema.required : [],
607
+ Array.isArray(basePropSchema.required)
608
+ ? basePropSchema.required
609
+ : [],
572
610
  );
573
611
  fields.push({
574
612
  name: fullName,
@@ -591,7 +629,9 @@ function extractFieldsFromSchema(
591
629
  const itemFields = extractFieldsFromSchema(
592
630
  baseItemSchema,
593
631
  `${fullName}[]`,
594
- Array.isArray(baseItemSchema.required) ? baseItemSchema.required : [],
632
+ Array.isArray(baseItemSchema.required)
633
+ ? baseItemSchema.required
634
+ : [],
595
635
  );
596
636
  const itemVariantData = extractVariantData(
597
637
  itemSchema,
@@ -643,14 +683,18 @@ function extractFieldsFromSchema(
643
683
  ? extractFieldsFromSchema(
644
684
  additionalSchema,
645
685
  mapFieldName,
646
- Array.isArray(additionalSchema?.required) ? additionalSchema.required : [],
686
+ Array.isArray(additionalSchema?.required)
687
+ ? additionalSchema.required
688
+ : [],
647
689
  )
648
690
  : [];
649
691
  const additionalVariantData = additionalSchema
650
692
  ? extractVariantData(
651
693
  additionalSchema,
652
694
  mapFieldName,
653
- Array.isArray(additionalSchema?.required) ? additionalSchema.required : [],
695
+ Array.isArray(additionalSchema?.required)
696
+ ? additionalSchema.required
697
+ : [],
654
698
  )
655
699
  : {};
656
700
 
@@ -664,7 +708,9 @@ function extractFieldsFromSchema(
664
708
  nested: additionalContent.length > 0 ? additionalContent : undefined,
665
709
  variants: additionalVariantData.variants,
666
710
  variantType: additionalVariantData.variantType,
667
- enum: additionalSchema ? getSchemaEnumValues(additionalSchema) : undefined,
711
+ enum: additionalSchema
712
+ ? getSchemaEnumValues(additionalSchema)
713
+ : undefined,
668
714
  });
669
715
  }
670
716
 
@@ -688,10 +734,13 @@ Object.entries(responses)
688
734
  return numA - numB;
689
735
  })
690
736
  .forEach(([statusCode, response]: [string, any]) => {
737
+ const binarySummary = getBinaryResponseSummary(response);
691
738
  // Try to get application/json content first
692
- const contentType = response.content?.["application/json"]
693
- ? "application/json"
694
- : Object.keys(response.content || {})[0];
739
+ const contentType =
740
+ binarySummary?.contentTypes[0] ||
741
+ (response.content?.["application/json"]
742
+ ? "application/json"
743
+ : Object.keys(response.content || {})[0]);
695
744
 
696
745
  const mediaType = contentType ? response.content?.[contentType] : null;
697
746
  const schema = mediaType?.schema;
@@ -700,7 +749,7 @@ Object.entries(responses)
700
749
  let variants: ResponseVariant[] | undefined;
701
750
  let variantType: "oneOf" | "anyOf" | undefined;
702
751
 
703
- if (schema) {
752
+ if (schema && !binarySummary) {
704
753
  const hasOneOf = Array.isArray(schema.oneOf) && schema.oneOf.length > 0;
705
754
  const hasAnyOf = Array.isArray(schema.anyOf) && schema.anyOf.length > 0;
706
755
 
@@ -720,7 +769,12 @@ Object.entries(responses)
720
769
 
721
770
  variants = variantSchemas
722
771
  .map((variantSchema, index) => ({
723
- label: `Variant ${index + 1}`,
772
+ label: getSchemaVariantLabel(variantSchema, index, {
773
+ parentSchema: schema,
774
+ siblings: variantSchemas,
775
+ getObjectProperties: getSchemaObjectProperties,
776
+ getTypeLabel: getSchemaTypeLabel,
777
+ }),
724
778
  fields: extractFieldsFromSchema(
725
779
  variantSchema,
726
780
  "",
@@ -739,6 +793,7 @@ Object.entries(responses)
739
793
  fields,
740
794
  variants,
741
795
  variantType,
796
+ binarySummary,
742
797
  });
743
798
  });
744
799
  ---
@@ -769,12 +824,12 @@ Object.entries(responses)
769
824
  {response.fields.length > 0 && (
770
825
  <>
771
826
  {response.variants && response.variants.length > 0 && (
772
- <p class="mb-2 text-xs text-neutral-500 dark:text-neutral-400">Common fields</p>
827
+ <p class="mb-2 text-xs text-neutral-500 dark:text-neutral-400">
828
+ Common fields
829
+ </p>
773
830
  )}
774
831
  <div x-data="{ expanded: true }">
775
- <div
776
- class="w-full overflow-hidden rounded-xl 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)]"
777
- >
832
+ <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)]">
778
833
  <button
779
834
  type="button"
780
835
  x-on:click="expanded = !expanded"
@@ -785,9 +840,7 @@ Object.entries(responses)
785
840
  <ListChevronsToggle class="size-4 shrink-0 text-neutral-500 group-hover:text-neutral-600 transition duration-200 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
786
841
  <span>
787
842
  {response.fields.length}{" "}
788
- {response.fields.length === 1
789
- ? "field"
790
- : "fields"}
843
+ {response.fields.length === 1 ? "field" : "fields"}
791
844
  </span>
792
845
  </span>
793
846
  </button>
@@ -814,9 +867,7 @@ Object.entries(responses)
814
867
  {variant.label}
815
868
  </div>
816
869
  <div x-data="{ expanded: true }">
817
- <div
818
- 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)]"
819
- >
870
+ <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)]">
820
871
  <button
821
872
  type="button"
822
873
  x-on:click="expanded = !expanded"
@@ -827,9 +878,7 @@ Object.entries(responses)
827
878
  <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200 dark:text-neutral-500 dark:group-hover:text-neutral-300" />
828
879
  <span>
829
880
  {variant.fields.length}{" "}
830
- {variant.fields.length === 1
831
- ? "field"
832
- : "fields"}
881
+ {variant.fields.length === 1 ? "field" : "fields"}
833
882
  </span>
834
883
  </span>
835
884
  </button>
@@ -857,7 +906,27 @@ Object.entries(responses)
857
906
  )}
858
907
  {response.fields.length === 0 &&
859
908
  (!response.variants || response.variants.length === 0) ? (
860
- <p class="text-sm text-neutral-500 ml-2 dark:text-neutral-400">No response body fields</p>
909
+ response.binarySummary ? (
910
+ <div class="ml-2 space-y-2 text-sm">
911
+ <div class="font-medium text-neutral-800 dark:text-neutral-200">
912
+ {response.binarySummary.title}
913
+ </div>
914
+ <p class="text-neutral-500 dark:text-neutral-400">
915
+ {response.binarySummary.description}
916
+ </p>
917
+ <div class="flex flex-wrap gap-1.5">
918
+ {response.binarySummary.contentTypes.map((contentType) => (
919
+ <code class="rounded-sm border border-neutral-900/4 bg-neutral-100/80 px-[5px] py-[2px] font-mono text-[11px] leading-none text-neutral-700/90 dark:border-white/3 dark:bg-neutral-800/90 dark:text-neutral-200/90">
920
+ {contentType}
921
+ </code>
922
+ ))}
923
+ </div>
924
+ </div>
925
+ ) : (
926
+ <p class="text-sm text-neutral-500 ml-2 dark:text-neutral-400">
927
+ No structured response body fields
928
+ </p>
929
+ )
861
930
  ) : null}
862
931
  </div>
863
932
  ))
@@ -8,6 +8,7 @@ import {
8
8
  getCodeLineTokens,
9
9
  normalizeCodeLanguageValue,
10
10
  } from "../../lib/code/code-block";
11
+ import { getBinaryResponseSummary } from "../../lib/openapi/response-content";
11
12
 
12
13
  interface Props {
13
14
  responses: OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject;
@@ -20,19 +21,23 @@ const sampledResponses: Array<{
20
21
  description: string;
21
22
  example?: unknown;
22
23
  contentType?: string;
24
+ binarySummaryText?: string;
23
25
  }> = [];
24
26
 
25
27
  Object.entries(responses).forEach(([statusCode, response]: [string, any]) => {
28
+ const binarySummary = getBinaryResponseSummary(response);
26
29
  const contentTypes = Object.keys(response.content || {});
27
- const contentType = response.content?.["application/json"]
28
- ? "application/json"
29
- : contentTypes[0];
30
+ const contentType =
31
+ binarySummary?.contentTypes[0] ||
32
+ (response.content?.["application/json"] ? "application/json" : contentTypes[0]);
30
33
 
31
34
  const mediaType = contentType ? response.content?.[contentType] : null;
32
35
  const schema = mediaType?.schema;
33
36
 
34
37
  let example: unknown = undefined;
35
- if (schema) {
38
+ if (binarySummary) {
39
+ example = binarySummary.snippetText;
40
+ } else if (schema) {
36
41
  try {
37
42
  example = Sampler.sample(schema as JSONSchema7, {
38
43
  skipReadOnly: false,
@@ -47,6 +52,7 @@ Object.entries(responses).forEach(([statusCode, response]: [string, any]) => {
47
52
  description: response.description ?? "",
48
53
  example,
49
54
  contentType,
55
+ binarySummaryText: binarySummary?.snippetText,
50
56
  });
51
57
  });
52
58
 
@@ -194,11 +200,12 @@ const responseSnippetItems = await Promise.all(
194
200
  ? "json"
195
201
  : "plaintext";
196
202
  const code =
197
- response.example === undefined
203
+ response.binarySummaryText ??
204
+ (response.example === undefined
198
205
  ? "This response has no body data."
199
206
  : typeof response.example === "string"
200
207
  ? response.example
201
- : JSON.stringify(response.example, null, 2);
208
+ : JSON.stringify(response.example, null, 2));
202
209
 
203
210
  return {
204
211
  statusCode: response.statusCode,
@@ -430,7 +437,11 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
430
437
  class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col rounded-xl"
431
438
  >
432
439
  <div
433
- class="flex items-center justify-between gap-2"
440
+ class:list={[
441
+ "flex items-center justify-between gap-2",
442
+ !hasMultipleResponses &&
443
+ "border-b-[0.5px] border-(--rd-code-tab-edge-border)",
444
+ ]}
434
445
  >
435
446
  {
436
447
  hasMultipleResponses ? (
@@ -517,6 +528,7 @@ const hasMultipleResponses = responseSnippetItems.length > 1;
517
528
  class:list={[
518
529
  "relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl border-[0.5px] border-(--rd-code-tab-edge-border) bg-(--rd-code-surface)",
519
530
  hasMultipleResponses && "rounded-tl-xl",
531
+ !hasMultipleResponses && "border-t-0",
520
532
  ]}
521
533
  >
522
534
  <div
@@ -384,5 +384,99 @@ const tabsPresentation = navigationTabs?.presentation ?? "topbar";
384
384
  );
385
385
  }
386
386
  </style>
387
+ <script is:inline>
388
+ (() => {
389
+ const initializedKey = "__radiantDocsSidebarScrollPersistence";
390
+ const storageKey = "radiant-docs:sidebar-scroll-top";
391
+ const selector = "[data-rd-sidebar-scroll-container]";
392
+
393
+ const getVisibleSidebar = () => {
394
+ const sidebars = document.querySelectorAll(selector);
395
+
396
+ for (const sidebar of sidebars) {
397
+ if (!(sidebar instanceof HTMLElement)) continue;
398
+
399
+ const rect = sidebar.getBoundingClientRect();
400
+ if (rect.width > 0 && rect.height > 0) return sidebar;
401
+ }
402
+
403
+ return null;
404
+ };
405
+
406
+ const getSavedScrollTop = () => {
407
+ try {
408
+ const value = Number(sessionStorage.getItem(storageKey));
409
+ return Number.isFinite(value) && value > 0 ? value : 0;
410
+ } catch {
411
+ return 0;
412
+ }
413
+ };
414
+
415
+ const saveSidebarScroll = () => {
416
+ const sidebar = getVisibleSidebar();
417
+ if (!sidebar) return;
418
+
419
+ try {
420
+ sessionStorage.setItem(
421
+ storageKey,
422
+ String(Math.max(0, Math.round(sidebar.scrollTop))),
423
+ );
424
+ } catch {
425
+ // Storage can be unavailable in private browsing or locked-down embeds.
426
+ }
427
+ };
428
+
429
+ const restoreSidebarScroll = () => {
430
+ const sidebar = getVisibleSidebar();
431
+ if (!sidebar) return;
432
+
433
+ const savedScrollTop = getSavedScrollTop();
434
+ if (savedScrollTop <= 0) return;
435
+
436
+ sidebar.scrollTop = Math.min(
437
+ savedScrollTop,
438
+ Math.max(0, sidebar.scrollHeight - sidebar.clientHeight),
439
+ );
440
+ };
441
+
442
+ const scheduleSidebarScrollRestore = () => {
443
+ window.requestAnimationFrame(() => {
444
+ restoreSidebarScroll();
445
+ window.setTimeout(restoreSidebarScroll, 0);
446
+ });
447
+ };
448
+
449
+ if (window[initializedKey]) {
450
+ scheduleSidebarScrollRestore();
451
+ return;
452
+ }
453
+
454
+ window[initializedKey] = true;
455
+ document.addEventListener(
456
+ "astro:before-preparation",
457
+ saveSidebarScroll,
458
+ );
459
+ document.addEventListener("astro:before-swap", saveSidebarScroll);
460
+ document.addEventListener(
461
+ "astro:after-swap",
462
+ scheduleSidebarScrollRestore,
463
+ );
464
+ document.addEventListener(
465
+ "astro:page-load",
466
+ scheduleSidebarScrollRestore,
467
+ );
468
+ window.addEventListener("pagehide", saveSidebarScroll);
469
+
470
+ if (document.readyState === "loading") {
471
+ document.addEventListener(
472
+ "DOMContentLoaded",
473
+ scheduleSidebarScrollRestore,
474
+ { once: true },
475
+ );
476
+ } else {
477
+ scheduleSidebarScrollRestore();
478
+ }
479
+ })();
480
+ </script>
387
481
  </body>
388
482
  </html>
@@ -8,6 +8,8 @@ import {
8
8
 
9
9
  export type ClientShikiRuntimeConfig = AssistantShikiRuntimeConfig;
10
10
 
11
+ const DEFAULT_SHIKI_PLATFORM_ASSET_HOST = "https://static.radiantdocs.com";
12
+
11
13
  function normalizeStaticAssetHost(value: unknown): string {
12
14
  const rawValue = typeof value === "string" ? value.trim() : "";
13
15
  if (!rawValue) return "";
@@ -43,9 +45,10 @@ function getConfiguredCodeSyntaxThemes(
43
45
  export function getClientShikiRuntimeConfig(
44
46
  config: DocsConfig,
45
47
  ): ClientShikiRuntimeConfig | undefined {
46
- const staticAssetHost = normalizeStaticAssetHost(
47
- import.meta.env.STATIC_ASSET_HOST ?? process.env.STATIC_ASSET_HOST,
48
- );
48
+ const staticAssetHost =
49
+ normalizeStaticAssetHost(
50
+ import.meta.env.STATIC_ASSET_HOST ?? process.env.STATIC_ASSET_HOST,
51
+ ) || DEFAULT_SHIKI_PLATFORM_ASSET_HOST;
49
52
  if (!staticAssetHost) {
50
53
  return undefined;
51
54
  }