radiant-docs 0.1.6 → 0.1.8

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 (78) hide show
  1. package/dist/index.js +32 -6
  2. package/package.json +3 -3
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -1,11 +1,15 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
3
  import type { OpenApiRoute } from "../../lib/routes";
4
+ import { getConfig } from "../../lib/validation";
4
5
  import { renderMarkdown } from "../../lib/utils";
5
- import { headers, type RequestFields } from "../OpenApiPage.astro";
6
- import Accordian from "../user/Accordian.astro";
6
+ import {
7
+ headers,
8
+ type RequestFields,
9
+ type RequestSectionVariantData,
10
+ } from "../OpenApiPage.astro";
11
+ import Accordion from "../user/Accordion.astro";
7
12
  import PlaygroundBar from "./PlaygroundBar.astro";
8
- import Field from "../ui/Field.astro";
9
13
  import ResponseDisplay from "./ResponseDisplay.astro";
10
14
  import PlaygroundField from "./PlaygroundField.astro";
11
15
 
@@ -13,37 +17,410 @@ interface Props {
13
17
  route: OpenApiRoute;
14
18
  serverUrl?: string;
15
19
  requestFields: RequestFields;
20
+ requestSectionVariants?: Partial<
21
+ Record<keyof RequestFields, RequestSectionVariantData>
22
+ >;
23
+ bodyDescription?: string;
24
+ bodyDefaultKind?: "object" | "array";
16
25
  }
17
26
 
18
- const { route, serverUrl, requestFields } = Astro.props;
27
+ const {
28
+ route,
29
+ serverUrl,
30
+ requestFields,
31
+ requestSectionVariants = {},
32
+ bodyDescription = "",
33
+ bodyDefaultKind,
34
+ } = Astro.props;
35
+ const config = await getConfig();
36
+ const proxyEnabled = config.playground?.proxy !== false;
37
+ const proxyUrl =
38
+ import.meta.env.PUBLIC_PROXY_URL ||
39
+ "https://docs-proxy.stefanjoseph-dev.workers.dev";
40
+ const formattedBodyDescription = bodyDescription
41
+ ? await renderMarkdown(bodyDescription)
42
+ : null;
43
+ const queryFieldMeta = Object.fromEntries(
44
+ requestFields.query.map((field) => [
45
+ field.name,
46
+ {
47
+ isArray: field.isArray === true,
48
+ isObject:
49
+ (field.nested && field.nested.length > 0) ||
50
+ /\bobject\b/.test(field.type),
51
+ style: field.style || "form",
52
+ explode: field.explode,
53
+ },
54
+ ]),
55
+ );
56
+ const sectionVariantFieldNames = Object.fromEntries(
57
+ Object.entries(requestSectionVariants).map(([section, data]) => [
58
+ section,
59
+ (data?.variants || []).map((variant) =>
60
+ variant.fields.map((field) => field.name),
61
+ ),
62
+ ]),
63
+ );
19
64
  ---
20
65
 
21
66
  <div
22
67
  x-data=`{
23
68
  loading: false,
24
69
  response: null,
70
+ queryFieldMeta: ${JSON.stringify(queryFieldMeta)},
71
+ sectionVariantFieldNames: ${JSON.stringify(sectionVariantFieldNames)},
72
+ bodyDefaultKind: ${JSON.stringify(bodyDefaultKind ?? null)},
73
+ selectedSectionVariants: {},
25
74
  inputs: {
26
75
  header: {},
27
76
  path: {},
28
77
  query: {},
78
+ cookie: {},
29
79
  body: {}
30
80
  },
31
- async sendRequest() {
81
+ getSectionVariantFieldNames(section, variantIndex) {
82
+ const sectionVariants = this.sectionVariantFieldNames[section] || [];
83
+ const variantNames = sectionVariants[variantIndex];
84
+ return Array.isArray(variantNames) ? variantNames : [];
85
+ },
86
+ detectSectionVariant(section) {
87
+ const target = this.inputs[section];
88
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
89
+ return 0;
90
+ }
91
+
92
+ const sectionVariants = this.sectionVariantFieldNames[section] || [];
93
+ let selectedIndex = 0;
94
+ let bestScore = -1;
95
+
96
+ sectionVariants.forEach((fieldNames, variantIndex) => {
97
+ if (!Array.isArray(fieldNames) || fieldNames.length === 0) return;
98
+ const score = fieldNames.reduce((total, fieldName) => {
99
+ return total + (Object.prototype.hasOwnProperty.call(target, fieldName) ? 1 : 0);
100
+ }, 0);
101
+ if (score > bestScore) {
102
+ bestScore = score;
103
+ selectedIndex = variantIndex;
104
+ }
105
+ });
106
+
107
+ return selectedIndex;
108
+ },
109
+ getSelectedSectionVariant(section) {
110
+ if (Object.prototype.hasOwnProperty.call(this.selectedSectionVariants, section)) {
111
+ return this.selectedSectionVariants[section];
112
+ }
113
+ const detected = this.detectSectionVariant(section);
114
+ this.selectedSectionVariants[section] = detected;
115
+ return detected;
116
+ },
117
+ pruneSectionForVariant(section, selectedVariantIndex) {
118
+ if (
119
+ !this.inputs[section] ||
120
+ typeof this.inputs[section] !== 'object' ||
121
+ Array.isArray(this.inputs[section])
122
+ ) {
123
+ this.inputs[section] = {};
124
+ }
125
+ const target = this.inputs[section];
126
+ const selectedNames = new Set(
127
+ this.getSectionVariantFieldNames(section, selectedVariantIndex),
128
+ );
129
+ const namesToRemove = new Set();
130
+ const sectionVariants = this.sectionVariantFieldNames[section] || [];
131
+
132
+ sectionVariants.forEach((fieldNames, variantIndex) => {
133
+ if (!Array.isArray(fieldNames) || variantIndex === selectedVariantIndex) return;
134
+ fieldNames.forEach((fieldName) => {
135
+ if (!selectedNames.has(fieldName)) namesToRemove.add(fieldName);
136
+ });
137
+ });
138
+
139
+ namesToRemove.forEach((fieldName) => {
140
+ delete target[fieldName];
141
+ });
142
+ },
143
+ selectSectionVariant(section, variantIndex) {
144
+ this.selectedSectionVariants[section] = variantIndex;
145
+ this.pruneSectionForVariant(section, variantIndex);
146
+ },
147
+ resolvePathTokenValue(tokenName) {
148
+ const rawValue = this.inputs?.path?.[tokenName];
149
+ if (rawValue !== undefined && rawValue !== null) {
150
+ const value = String(rawValue).trim();
151
+ if (value.length > 0) return encodeURIComponent(value);
152
+ }
153
+
154
+ // Keep request URL valid even when server variables (e.g. {apiKey}) are unset,
155
+ // so the target API can return its own auth/validation error response.
156
+ if (/(^|[_-])(api[_-]?key|token|secret|key)$/i.test(String(tokenName))) {
157
+ return "invalid";
158
+ }
159
+
160
+ return "invalid";
161
+ },
162
+ resolveUrlPlaceholders(urlTemplate) {
163
+ return String(urlTemplate).replace(/\{([^}]+)\}/g, (_match, tokenName) =>
164
+ this.resolvePathTokenValue(String(tokenName)),
165
+ );
166
+ },
167
+ sanitizeBodyValue(value) {
168
+ if (value === undefined || value === null || value === '') {
169
+ return undefined;
170
+ }
171
+
172
+ if (Array.isArray(value)) {
173
+ const cleaned = value
174
+ .map((item) => this.sanitizeBodyValue(item))
175
+ .filter((item) => item !== undefined);
176
+ return cleaned.length > 0 ? cleaned : undefined;
177
+ }
178
+
179
+ if (typeof value === 'object') {
180
+ const cleanedEntries = Object.entries(value)
181
+ .map(([key, item]) => [key, this.sanitizeBodyValue(item)])
182
+ .filter(([, item]) => item !== undefined);
183
+
184
+ if (cleanedEntries.length === 0) return undefined;
185
+ return Object.fromEntries(cleanedEntries);
186
+ }
187
+
188
+ return value;
189
+ },
190
+ isPlainObject(value) {
191
+ return (
192
+ value !== null &&
193
+ typeof value === 'object' &&
194
+ !Array.isArray(value)
195
+ );
196
+ },
197
+ serializeQueryObject(params, key, rawValue, meta = {}) {
198
+ const cleanedObject = this.sanitizeBodyValue(rawValue);
199
+ if (!this.isPlainObject(cleanedObject)) return;
200
+
201
+ const entries = Object.entries(cleanedObject);
202
+ if (entries.length === 0) return;
203
+
204
+ const style = meta.style || 'form';
205
+ const explode =
206
+ typeof meta.explode === 'boolean' ? meta.explode : style === 'form';
207
+
208
+ if (style === 'deepObject') {
209
+ const appendDeepValue = (prefix, value) => {
210
+ if (value === undefined || value === null || value === '') return;
211
+
212
+ if (Array.isArray(value)) {
213
+ value.forEach((item, index) => {
214
+ appendDeepValue(prefix + '[' + index + ']', item);
215
+ });
216
+ return;
217
+ }
218
+
219
+ if (this.isPlainObject(value)) {
220
+ Object.entries(value).forEach(([nestedKey, nestedValue]) => {
221
+ appendDeepValue(prefix + '[' + nestedKey + ']', nestedValue);
222
+ });
223
+ return;
224
+ }
225
+
226
+ params.append(prefix, String(value));
227
+ };
228
+
229
+ entries.forEach(([propertyKey, propertyValue]) => {
230
+ appendDeepValue(key + '[' + propertyKey + ']', propertyValue);
231
+ });
232
+ return;
233
+ }
234
+
235
+ if (style === 'form' && explode) {
236
+ const appendExploded = (propertyKey, value) => {
237
+ if (value === undefined || value === null || value === '') return;
238
+
239
+ if (Array.isArray(value)) {
240
+ value.forEach((item) => appendExploded(propertyKey, item));
241
+ return;
242
+ }
243
+
244
+ if (this.isPlainObject(value)) {
245
+ params.append(propertyKey, JSON.stringify(value));
246
+ return;
247
+ }
248
+
249
+ params.append(propertyKey, String(value));
250
+ };
251
+
252
+ entries.forEach(([propertyKey, propertyValue]) => {
253
+ appendExploded(propertyKey, propertyValue);
254
+ });
255
+ return;
256
+ }
257
+
258
+ const flattenedPairs = [];
259
+ entries.forEach(([propertyKey, propertyValue]) => {
260
+ if (
261
+ propertyValue === undefined ||
262
+ propertyValue === null ||
263
+ propertyValue === ''
264
+ ) {
265
+ return;
266
+ }
267
+
268
+ let serializedValue = '';
269
+ if (Array.isArray(propertyValue)) {
270
+ serializedValue = propertyValue.map((item) => String(item)).join(',');
271
+ } else if (this.isPlainObject(propertyValue)) {
272
+ serializedValue = JSON.stringify(propertyValue);
273
+ } else {
274
+ serializedValue = String(propertyValue);
275
+ }
276
+
277
+ if (serializedValue === '') return;
278
+ flattenedPairs.push(propertyKey, serializedValue);
279
+ });
280
+
281
+ if (flattenedPairs.length === 0) return;
282
+
283
+ const delimiter =
284
+ style === 'spaceDelimited'
285
+ ? ' '
286
+ : style === 'pipeDelimited'
287
+ ? '|'
288
+ : ',';
289
+
290
+ params.append(key, flattenedPairs.join(delimiter));
291
+ },
292
+ getDefaultRequestBody() {
293
+ if (this.bodyDefaultKind === 'object') return {};
294
+ if (this.bodyDefaultKind === 'array') return [];
295
+ return undefined;
296
+ },
297
+ buildCookieHeader() {
298
+ const cookieInputs = this.inputs?.cookie;
299
+ if (
300
+ !cookieInputs ||
301
+ typeof cookieInputs !== 'object' ||
302
+ Array.isArray(cookieInputs)
303
+ ) {
304
+ return '';
305
+ }
306
+
307
+ const cookieParts = [];
308
+ Object.entries(cookieInputs).forEach(([key, rawValue]) => {
309
+ const cleanedValue = this.sanitizeBodyValue(rawValue);
310
+ if (cleanedValue === undefined) return;
311
+
312
+ let valueString = '';
313
+ if (Array.isArray(cleanedValue)) {
314
+ valueString = cleanedValue.map((item) => String(item)).join(',');
315
+ } else if (typeof cleanedValue === 'object') {
316
+ valueString = JSON.stringify(cleanedValue);
317
+ } else {
318
+ valueString = String(cleanedValue);
319
+ }
320
+
321
+ if (valueString.length === 0) return;
322
+ cookieParts.push(key + '=' + encodeURIComponent(valueString));
323
+ });
324
+
325
+ return cookieParts.join('; ');
326
+ },
327
+ buildQueryParams() {
328
+ const params = new URLSearchParams();
329
+
330
+ Object.entries(this.inputs.query || {}).forEach(([key, rawValue]) => {
331
+ const meta = this.queryFieldMeta[key] || {};
332
+
333
+ if (Array.isArray(rawValue) || meta.isArray) {
334
+ const values = (Array.isArray(rawValue) ? rawValue : [rawValue])
335
+ .map((value) => (value === undefined || value === null ? '' : String(value)))
336
+ .filter((value) => value !== '');
337
+
338
+ if (values.length === 0) return;
339
+
340
+ const style = meta.style || 'form';
341
+ const explode =
342
+ typeof meta.explode === 'boolean' ? meta.explode : style === 'form';
343
+
344
+ if (style === 'form') {
345
+ if (explode) {
346
+ values.forEach((value) => params.append(key, value));
347
+ } else {
348
+ params.append(key, values.join(','));
349
+ }
350
+ return;
351
+ }
352
+
353
+ if (style === 'spaceDelimited') {
354
+ params.append(key, values.join(' '));
355
+ return;
356
+ }
357
+
358
+ if (style === 'pipeDelimited') {
359
+ params.append(key, values.join('|'));
360
+ return;
361
+ }
362
+
363
+ values.forEach((value) => params.append(key, value));
364
+ return;
365
+ }
366
+
367
+ if (this.isPlainObject(rawValue) || meta.isObject) {
368
+ this.serializeQueryObject(params, key, rawValue, meta);
369
+ return;
370
+ }
371
+
372
+ if (rawValue === undefined || rawValue === null) return;
373
+ const value = String(rawValue);
374
+ if (value === '') return;
375
+ params.append(key, value);
376
+ });
377
+
378
+ return params.toString();
379
+ },
380
+ validateInputs(triggerEl) {
381
+ const rootFromTrigger =
382
+ triggerEl instanceof HTMLElement
383
+ ? triggerEl.closest('[data-playground-form-root]')
384
+ : null;
385
+ const root = rootFromTrigger || this.$root || this.$el;
386
+ if (!(root instanceof HTMLElement)) return true;
387
+
388
+ const controls = root.querySelectorAll('input, select, textarea');
389
+ for (const control of controls) {
390
+ if (
391
+ !(control instanceof HTMLInputElement) &&
392
+ !(control instanceof HTMLSelectElement) &&
393
+ !(control instanceof HTMLTextAreaElement)
394
+ ) {
395
+ continue;
396
+ }
397
+ if (control.disabled) continue;
398
+ if (control.type === 'hidden') continue;
399
+ if (control.getClientRects().length === 0) continue;
400
+
401
+ if (!control.reportValidity()) {
402
+ control.focus();
403
+ control.scrollIntoView({ block: 'center', behavior: 'smooth' });
404
+ return false;
405
+ }
406
+ }
407
+
408
+ return true;
409
+ },
410
+ async sendRequest(event) {
411
+ if (!this.validateInputs(event?.currentTarget)) return;
412
+
32
413
  this.loading = true;
33
- await new Promise(resolve => setTimeout(resolve, 2000))
34
414
  try {
35
415
  // 1. Construct URL with Path Parameters
36
- let url = ${JSON.stringify(serverUrl + route.openApiPath)};
37
- Object.entries(this.inputs.path).forEach(([key, val]) => {
38
- url = url.replace('{' + key + '}', encodeURIComponent(val));
39
- });
416
+ const urlTemplate = ${JSON.stringify(serverUrl + route.openApiPath)};
417
+ let url = this.resolveUrlPlaceholders(urlTemplate);
40
418
 
41
419
  // 2. Add Query Parameters
42
- const queryParams = new URLSearchParams(this.inputs.query).toString();
420
+ const queryParams = this.buildQueryParams();
43
421
  if (queryParams) url += '?' + queryParams;
44
422
 
45
423
  // 3. Prepare Fetch Options
46
- console.log("Hello", this.inputs)
47
424
  const options = {
48
425
  method: ${JSON.stringify(route.openApiMethod.toUpperCase())},
49
426
  headers: {
@@ -51,87 +428,117 @@ const { route, serverUrl, requestFields } = Astro.props;
51
428
  ...this.inputs.header
52
429
  }
53
430
  };
431
+ const cookieHeader = this.buildCookieHeader();
54
432
 
55
433
  // 4. Add Body if needed
56
434
  if (['POST', 'PUT', 'PATCH'].includes(options.method)) {
57
- options.body = JSON.stringify(this.inputs.body);
435
+ const sanitizedBody = this.sanitizeBodyValue(this.inputs.body);
436
+ if (sanitizedBody !== undefined) {
437
+ options.body = JSON.stringify(sanitizedBody);
438
+ } else {
439
+ const defaultBody = this.getDefaultRequestBody();
440
+ if (defaultBody !== undefined) {
441
+ options.body = JSON.stringify(defaultBody);
442
+ }
443
+ }
58
444
  }
59
445
 
60
- // 5. Execute Request
61
- const res = await fetch(url, options);
446
+ // 5. Proxy Logic
447
+ let finalUrl = url;
448
+ let finalOptions = options;
62
449
 
63
- const status = res.status;
64
- const statusTextMap = {
65
- 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden',
66
- 404: 'Not Found', 405: 'Method Not Allowed', 409: 'Conflict',
67
- 422: 'Unprocessable Entity', 429: 'Too Many Requests',
68
- 500: 'Internal Server Error', 502: 'Bad Gateway',
69
- 503: 'Service Unavailable', 504: 'Gateway Timeout'
450
+ const isLocal = (url) => {
451
+ try {
452
+ const hostname = new URL(url).hostname;
453
+ return hostname === 'localhost' ||
454
+ hostname === '127.0.0.1' ||
455
+ hostname.endsWith('.local');
456
+ } catch (e) {
457
+ return false;
458
+ }
70
459
  };
71
- const statusText = res.statusText || statusTextMap[status] || 'Unknown';
72
460
 
73
- const headers = {};
461
+ if (${proxyEnabled} && !isLocal(url)) {
462
+ finalUrl = ${JSON.stringify(proxyUrl)};
463
+ finalOptions = {
464
+ ...options,
465
+ headers: {
466
+ ...options.headers,
467
+ 'x-proxy-url': url,
468
+ ...(cookieHeader ? { 'x-proxy-cookie': cookieHeader } : {})
469
+ }
470
+ };
471
+ }
472
+ console.log("SEND", finalUrl, finalOptions)
473
+ // 6. Execute Request
474
+ const res = await fetch(finalUrl, finalOptions);
475
+
476
+ const status = res.status;
477
+
478
+ const responseHeaders = {};
74
479
  res.headers.forEach((value, key) => {
75
- headers[key] = value;
480
+ responseHeaders[key] = value;
76
481
  });
77
482
 
78
- let data;
79
483
  let highlightedData;
484
+ const highlightedHeaders = Prism.highlight(
485
+ JSON.stringify(responseHeaders, null, 2),
486
+ Prism.languages.json,
487
+ "json",
488
+ );
80
489
  const contentType = res.headers.get('content-type') || '';
490
+ const responseText = await res.text();
81
491
 
82
- try {
83
- if (contentType.includes('application/json')) {
84
- data = await res.json();
492
+ if (contentType.includes('application/json')) {
493
+ try {
494
+ const parsed =
495
+ responseText.trim().length > 0 ? JSON.parse(responseText) : null;
85
496
  highlightedData = Prism.highlight(
86
- JSON.stringify(data, null, 2),
87
- Prism.languages.json,
497
+ JSON.stringify(parsed, null, 2),
498
+ Prism.languages.json,
88
499
  'json'
89
500
  );
90
- } else {
91
- // For non-JSON responses, try to get as text
92
- const text = await res.text();
93
- console.log("TEXT", text)
501
+ } catch (parseError) {
94
502
  highlightedData = Prism.highlight(
95
- text || '(empty response)',
96
- Prism.languages.plaintext,
503
+ responseText || '(empty response)',
504
+ Prism.languages.plaintext,
97
505
  'plaintext'
98
506
  );
99
507
  }
100
- } catch (parseError) {
101
- // If parsing fails, still show the response with error info
508
+ } else {
102
509
  highlightedData = Prism.highlight(
103
- JSON.stringify({ error: 'Failed to parse response', message: parseError.message }, null, 2),
104
- Prism.languages.json,
105
- 'json'
510
+ responseText || '(empty response)',
511
+ Prism.languages.plaintext,
512
+ 'plaintext'
106
513
  );
107
514
  }
108
515
 
109
516
  this.response = {
110
517
  status,
111
- statusText,
112
- headers,
113
- highlightedData
518
+ headers: responseHeaders,
519
+ highlightedData,
520
+ highlightedHeaders
114
521
  };
115
522
 
116
523
  } catch (err) {
117
- console.log('error', err)
524
+ console.error('Playground error:', err);
118
525
  this.response = {
119
526
  status: err.status || 0,
120
- statusText: err.statusText || err.message || 'Network Error',
121
527
  };
122
528
  } finally {
123
529
  this.loading = false;
124
530
  }
125
531
  }
126
532
  }`
127
- class="flex flex-col gap-4"
533
+ data-playground-form-root
534
+ class="flex w-full h-full min-h-0 flex-col"
128
535
  >
129
- <div class="w-full">
536
+
130
537
  <PlaygroundBar route={route} serverUrl={serverUrl}>
131
538
  <button
132
- @click="sendRequest()"
539
+ @click="sendRequest($event)"
133
540
  :disabled="loading"
134
- class="m-px flex items-center gap-1.5 px-4 py-[5px] text-sm font-medium rounded-lg bg-neutral-900 text-white/95 hover:text-white shadow-[inset_0_1px_0_rgb(255,255,255,0.3),0_0_0_1px_var(--color-neutral-800)] duration-200 whitespace-nowrap cursor-pointer relative before:absolute before:inset-0 before:shadow-sm before:rounded-lg before:bg-transparent disabled:bg-neutral-300 disabled:shadow-none disabled:before:shadow-none disabled:duration-0"
541
+ class="m-px flex items-center gap-1.5 px-4 py-[5px] text-sm font-medium rounded-lg bg-neutral-900 text-white/95 hover:text-white shadow-[inset_0_1px_0_rgb(255,255,255,0.3),0_0_0_1px_var(--color-neutral-800)] duration-200 whitespace-nowrap cursor-pointer relative before:absolute before:inset-0 before:shadow-sm before:rounded-lg before:bg-transparent disabled:opacity-70"
135
542
  >
136
543
  <span
137
544
  class="flex items-center gap-2"
@@ -147,31 +554,122 @@ const { route, serverUrl, requestFields } = Astro.props;
147
554
  />
148
555
  </button>
149
556
  </PlaygroundBar>
150
- </div>
151
557
 
152
- <div class="flex flex-col lg:flex-row-reverse gap-4">
153
- <div class="lg:max-w-xl w-full">
558
+
559
+ <div class="min-h-0 flex-1 flex flex-col lg:flex-row-reverse">
560
+ <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">
154
561
  <ResponseDisplay />
155
562
  </div>
156
- <div class="flex-3 space-y-4">
563
+ <div class="lg:flex-1 relative">
564
+ <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">
565
+ <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"></div>
566
+ <div class="space-y-4 lg:pr-4 pt-4 pb-6">
157
567
  {
158
568
  Object.keys(requestFields).map(
159
- (key) =>
160
- requestFields[key as keyof RequestFields].length > 0 && (
569
+ (key) => {
570
+ const sectionFields = requestFields[key as keyof RequestFields];
571
+ const sectionVariantData =
572
+ requestSectionVariants[key as keyof RequestFields];
573
+ const sectionVariants = sectionVariantData?.variants || [];
574
+ const keyLiteral = JSON.stringify(key);
575
+
576
+ if (sectionFields.length === 0 && sectionVariants.length === 0) {
577
+ return null;
578
+ }
579
+
580
+ return (
161
581
  <div class="border border-neutral-200 shadow-xs rounded-xl p-4">
162
- <Accordian title={headers[key]} defaultOpen titleSize="xl">
163
- {requestFields[key as keyof RequestFields].map((field) => (
582
+ <Accordion title={headers[key]} defaultOpen titleSize="xl">
583
+ {key === "body" && formattedBodyDescription && (
584
+ <div
585
+ class="mb-4 prose-rules prose-sm! text-neutral-500 **:text-neutral-500"
586
+ set:html={formattedBodyDescription}
587
+ />
588
+ )}
589
+ {sectionFields.map((field) => (
164
590
  <div class="border-b border-b-neutral-100 last:border-none pb-4 mb-4 last:pb-0 last:mb-0 first:pt-2">
165
591
  <PlaygroundField field={field} requestPart={key} />
166
592
  </div>
167
593
  ))}
168
- </Accordian>
594
+ {sectionVariants.length > 0 && (
595
+ <div class:list={["space-y-2", sectionFields.length > 0 && "pt-1"]}>
596
+ {sectionVariantData?.variantType === "oneOf" ? (
597
+ <>
598
+ <p class="text-xs text-neutral-500">
599
+ Select one variant.
600
+ </p>
601
+ <div class="inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg bg-neutral-100 p-1">
602
+ {sectionVariants.map((variant, variantIndex) => (
603
+ <button
604
+ type="button"
605
+ @click={`selectSectionVariant(${keyLiteral}, ${variantIndex})`}
606
+ :class={`{
607
+ 'bg-white text-neutral-900 shadow-xs ring-1 ring-neutral-200': getSelectedSectionVariant(${keyLiteral}) === ${variantIndex},
608
+ 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-800': getSelectedSectionVariant(${keyLiteral}) !== ${variantIndex}
609
+ }`}
610
+ 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"
611
+ >
612
+ {variant.label}
613
+ </button>
614
+ ))}
615
+ </div>
616
+ {sectionVariants.map((variant, variantIndex) => (
617
+ <div
618
+ x-show={`getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`}
619
+ x-cloak
620
+ >
621
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
622
+ <div class="mb-2 text-xs font-medium text-neutral-600">
623
+ {variant.label}
624
+ </div>
625
+ <div class="space-y-3">
626
+ {variant.fields.map((field) => (
627
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
628
+ <PlaygroundField
629
+ field={field}
630
+ requestPart={key}
631
+ defaultsEnabledExpr={`getSelectedSectionVariant(${keyLiteral}) === ${variantIndex}`}
632
+ />
633
+ </div>
634
+ ))}
635
+ </div>
636
+ </div>
637
+ </div>
638
+ ))}
639
+ </>
640
+ ) : (
641
+ <>
642
+ <p class="text-xs text-neutral-500">
643
+ One or more variants may apply.
644
+ </p>
645
+ {sectionVariants.map((variant) => (
646
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
647
+ <div class="mb-2 text-xs font-medium text-neutral-600">
648
+ {variant.label}
649
+ </div>
650
+ <div class="space-y-3">
651
+ {variant.fields.map((field) => (
652
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
653
+ <PlaygroundField field={field} requestPart={key} />
654
+ </div>
655
+ ))}
656
+ </div>
657
+ </div>
658
+ ))}
659
+ </>
660
+ )}
661
+ </div>
662
+ )}
663
+ </Accordion>
169
664
  </div>
170
- )
665
+ );
666
+ },
171
667
  )
172
668
  }
669
+ </div>
173
670
  </div>
174
671
  </div>
672
+ </div>
175
673
  </div>
176
674
 
177
675
  <script>