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
@@ -8,9 +8,16 @@ interface Props {
8
8
  requestPart: string;
9
9
  scopeExpr?: string;
10
10
  defaultsEnabledExpr?: string;
11
+ requiredEnabledExpr?: string;
11
12
  }
12
13
 
13
- const { field, requestPart, scopeExpr, defaultsEnabledExpr } = Astro.props;
14
+ const {
15
+ field,
16
+ requestPart,
17
+ scopeExpr,
18
+ defaultsEnabledExpr,
19
+ requiredEnabledExpr,
20
+ } = Astro.props;
14
21
 
15
22
  function cloneDefaultTemplate<T>(value: T): T {
16
23
  if (value === undefined) return value;
@@ -49,6 +56,16 @@ function mergeObjectDefaults(
49
56
  });
50
57
  }
51
58
 
59
+ function isObjectLikeOpenApiField(field: FieldType): boolean {
60
+ return (
61
+ field.isAdditionalProperty === true ||
62
+ Boolean(field.nested && field.nested.length > 0) ||
63
+ Boolean(field.variants && field.variants.length > 0) ||
64
+ Boolean(field.valueVariants && field.valueVariants.length > 0) ||
65
+ /\bobject\b/.test(field.type)
66
+ );
67
+ }
68
+
52
69
  function buildVariantDefaultTemplate(field: FieldType): Record<string, unknown> {
53
70
  const variants = field.variants || [];
54
71
  if (variants.length === 0) return {};
@@ -107,17 +124,34 @@ function buildFieldDefaultTemplate(field: FieldType): unknown | undefined {
107
124
  return Object.keys(objectDefaults).length > 0 ? objectDefaults : undefined;
108
125
  }
109
126
 
127
+ function buildFieldInitialValue(field: FieldType): unknown {
128
+ if (field.hasDefault) {
129
+ return cloneDefaultTemplate(field.defaultValue);
130
+ }
131
+
132
+ if (field.isArray) return [];
133
+ if (isObjectLikeOpenApiField(field)) return {};
134
+ return "";
135
+ }
136
+
110
137
  const requestPartKey = JSON.stringify(requestPart);
111
138
  const fieldNameKey = JSON.stringify(field.name);
112
139
  const defaultScopeExpr = `inputs[${requestPartKey}]`;
113
140
  const resolvedScopeExpr = scopeExpr || defaultScopeExpr;
114
141
  const resolvedDefaultsEnabledExpr = defaultsEnabledExpr || "true";
142
+ const resolvedRequiredEnabledExpr =
143
+ requiredEnabledExpr || resolvedDefaultsEnabledExpr;
115
144
  const fieldModelExpr = `${resolvedScopeExpr}[${fieldNameKey}]`;
116
145
  const isNumericType = /\b(number|integer)\b/.test(field.type);
117
146
  const isIntegerType = /\binteger\b/.test(field.type);
118
147
  const hasStringType = /\bstring\b/.test(field.type);
119
148
  const isBooleanType = /\bboolean\b/.test(field.type);
120
- const inputType = isNumericType && !hasStringType ? "number" : "text";
149
+ const isFileInput = field.isFile === true;
150
+ const inputType = isFileInput
151
+ ? "file"
152
+ : isNumericType && !hasStringType
153
+ ? "number"
154
+ : "text";
121
155
  const isRequiredField = field.required === true;
122
156
  const stringMinLength =
123
157
  typeof field.minLength === "number" &&
@@ -193,13 +227,35 @@ const stringMaxLengthLiteral =
193
227
  stringMaxLength !== undefined ? JSON.stringify(stringMaxLength) : "undefined";
194
228
  const hasNestedFields = Boolean(field.nested && field.nested.length > 0);
195
229
  const hasFieldVariants = Boolean(field.variants && field.variants.length > 0);
230
+ const valueVariants = field.valueVariants || [];
231
+ const hasValueVariants = valueVariants.length > 0;
196
232
  const hasOneOfVariants = field.variantType === "oneOf" && hasFieldVariants;
197
233
  const oneOfVariantFieldKeys = hasOneOfVariants
198
234
  ? (field.variants || []).map((variant) =>
199
235
  (variant.fields || []).map((variantField) => variantField.name),
200
236
  )
201
237
  : [];
238
+ const oneOfVariantDefaultTemplates = hasOneOfVariants
239
+ ? (field.variants || []).map((variant) =>
240
+ buildObjectDefaultTemplateFromFields(variant.fields),
241
+ )
242
+ : [];
243
+ const oneOfVariantDefaultTemplatesJson = JSON.stringify(
244
+ oneOfVariantDefaultTemplates,
245
+ ).replaceAll("`", "\\`");
246
+ const valueVariantMeta = valueVariants.map((variant) => ({
247
+ initialValue: buildFieldInitialValue(variant.field),
248
+ isArray: variant.field.isArray === true,
249
+ isObjectLike:
250
+ !variant.field.isArray && isObjectLikeOpenApiField(variant.field),
251
+ type: variant.field.type,
252
+ }));
253
+ const valueVariantMetaJson = JSON.stringify(valueVariantMeta).replaceAll(
254
+ "`",
255
+ "\\`",
256
+ );
202
257
  const isAdditionalPropertyField = field.isAdditionalProperty === true;
258
+ const renderAsJsonInput = field.renderAsJson === true && !field.isArray;
203
259
  const mapKnownKeys = JSON.stringify(field.mapKnownKeys || []);
204
260
  const oneOfVariantFieldKeysJson = JSON.stringify(oneOfVariantFieldKeys);
205
261
  const defaultTemplate = buildFieldDefaultTemplate(field);
@@ -212,15 +268,28 @@ const hasComplexMapValue =
212
268
  field.isArray ||
213
269
  hasNestedFields ||
214
270
  hasFieldVariants ||
271
+ hasValueVariants ||
272
+ renderAsJsonInput ||
215
273
  /\bobject\b/.test(field.type);
216
274
  const isObjectLikeField =
217
275
  isAdditionalPropertyField ||
218
276
  hasNestedFields ||
219
277
  hasFieldVariants ||
278
+ hasValueVariants ||
220
279
  /\bobject\b/.test(field.type);
221
280
  const layoutClass = isObjectLikeField
222
281
  ? "flex justify-between flex-col gap-4"
223
282
  : "flex justify-between flex-col sm:flex-row lg:flex-col xl:flex-row gap-4";
283
+ const fieldPlaceholder = field.valuePrefix
284
+ ? `Enter ${field.valuePrefix.toLowerCase()} token`
285
+ : `Enter ${field.name}`;
286
+ const fieldRequiredExpr = isRequiredField
287
+ ? `Boolean(${resolvedRequiredEnabledExpr})`
288
+ : "false";
289
+ const fieldValueGetterExpr = `readFieldValue(() => ${fieldModelExpr})`;
290
+ const childRequiredEnabledExpr = isRequiredField
291
+ ? `Boolean(${resolvedRequiredEnabledExpr})`
292
+ : `Boolean(${resolvedRequiredEnabledExpr}) && isRequiredScopeActive(${fieldValueGetterExpr})`;
224
293
  ---
225
294
 
226
295
  <div
@@ -245,6 +314,28 @@ const layoutClass = isObjectLikeField
245
314
  return value;
246
315
  }
247
316
  },
317
+ readFieldValue(getter) {
318
+ try {
319
+ return getter();
320
+ } catch (e) {
321
+ return undefined;
322
+ }
323
+ },
324
+ isRequiredScopeActive(value) {
325
+ if (value === undefined || value === null || value === '') return false;
326
+
327
+ if (Array.isArray(value)) {
328
+ return value.some((item) => this.isRequiredScopeActive(item));
329
+ }
330
+
331
+ if (typeof value === 'object') {
332
+ return Object.values(value).some((item) =>
333
+ this.isRequiredScopeActive(item),
334
+ );
335
+ }
336
+
337
+ return true;
338
+ },
248
339
  defaultsAreEnabled() {
249
340
  try {
250
341
  return Boolean(${resolvedDefaultsEnabledExpr});
@@ -279,8 +370,36 @@ const layoutClass = isObjectLikeField
279
370
  this.applyInitialDefault();
280
371
  },
281
372
  oneOfVariantFieldKeys: ${oneOfVariantFieldKeysJson},
373
+ oneOfVariantDefaultTemplates: ${oneOfVariantDefaultTemplatesJson},
374
+ valueVariantMeta: ${valueVariantMetaJson},
375
+ selectedValueVariant: null,
282
376
  selectedOneOfVariant: null,
283
377
  selectedArrayItemVariants: {},
378
+ arrayValueKeys: [],
379
+ arrayObjectKeys: [],
380
+ createArrayItemKey() {
381
+ return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
382
+ },
383
+ syncArrayKeys(keys, length) {
384
+ while (keys.length < length) {
385
+ keys.push(this.createArrayItemKey());
386
+ }
387
+ if (keys.length > length) {
388
+ keys.splice(length);
389
+ }
390
+ return keys;
391
+ },
392
+ getArrayValueKey(index) {
393
+ const values = ${fieldModelExpr};
394
+ const length = Array.isArray(values) ? values.length : 1;
395
+ const keys = this.syncArrayKeys(this.arrayValueKeys, length);
396
+ return keys[index] || String(index);
397
+ },
398
+ getArrayObjectKey(index) {
399
+ const values = this.getArrayObjectValues();
400
+ const keys = this.syncArrayKeys(this.arrayObjectKeys, values.length);
401
+ return keys[index] || String(index);
402
+ },
284
403
  ensureFieldObject() {
285
404
  this.ensureScopeObject();
286
405
  if (
@@ -330,6 +449,21 @@ const layoutClass = isObjectLikeField
330
449
  delete target[key];
331
450
  });
332
451
  },
452
+ applyObjectDefaults(target, defaults, overwrite = false) {
453
+ if (!target || typeof target !== 'object' || Array.isArray(target)) return;
454
+ if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) return;
455
+
456
+ Object.entries(defaults).forEach(([key, value]) => {
457
+ if (!overwrite && Object.prototype.hasOwnProperty.call(target, key)) {
458
+ return;
459
+ }
460
+ target[key] = this.cloneDefaultValue(value);
461
+ });
462
+ },
463
+ applyOneOfVariantDefaults(target, variantIndex, overwrite = false) {
464
+ const defaults = this.oneOfVariantDefaultTemplates[variantIndex];
465
+ this.applyObjectDefaults(target, defaults, overwrite);
466
+ },
333
467
  getSelectedOneOfVariant() {
334
468
  if (Number.isInteger(this.selectedOneOfVariant)) {
335
469
  return this.selectedOneOfVariant;
@@ -343,6 +477,68 @@ const layoutClass = isObjectLikeField
343
477
  this.selectedOneOfVariant = variantIndex;
344
478
  const target = this.ensureFieldObject();
345
479
  this.pruneObjectForOneOf(target, variantIndex);
480
+ this.applyOneOfVariantDefaults(target, variantIndex, true);
481
+ },
482
+ getValueVariantMeta(variantIndex) {
483
+ const meta = this.valueVariantMeta[variantIndex];
484
+ return meta && typeof meta === 'object' ? meta : {};
485
+ },
486
+ isScalarValueVariant(meta) {
487
+ return !meta.isArray && !meta.isObjectLike;
488
+ },
489
+ findValueVariantIndex(predicate) {
490
+ const index = this.valueVariantMeta.findIndex((meta) => predicate(meta));
491
+ return index >= 0 ? index : undefined;
492
+ },
493
+ detectValueVariant(value) {
494
+ if (!Array.isArray(this.valueVariantMeta) || this.valueVariantMeta.length === 0) {
495
+ return 0;
496
+ }
497
+
498
+ let matchedIndex;
499
+ if (Array.isArray(value)) {
500
+ matchedIndex = this.findValueVariantIndex((meta) => meta.isArray);
501
+ } else if (value && typeof value === 'object') {
502
+ matchedIndex = this.findValueVariantIndex((meta) => meta.isObjectLike);
503
+ } else if (typeof value === 'boolean') {
504
+ matchedIndex = this.findValueVariantIndex((meta) =>
505
+ String(meta.type || '').includes('boolean'),
506
+ );
507
+ } else if (typeof value === 'number') {
508
+ matchedIndex = this.findValueVariantIndex((meta) =>
509
+ /\b(number|integer)\b/.test(String(meta.type || '')),
510
+ );
511
+ } else {
512
+ matchedIndex = this.findValueVariantIndex((meta) =>
513
+ /\bstring\b/.test(String(meta.type || '')),
514
+ );
515
+ }
516
+
517
+ if (matchedIndex !== undefined) return matchedIndex;
518
+ matchedIndex = this.findValueVariantIndex((meta) =>
519
+ this.isScalarValueVariant(meta),
520
+ );
521
+ return matchedIndex !== undefined ? matchedIndex : 0;
522
+ },
523
+ cloneValueVariantInitialValue(variantIndex) {
524
+ const initialValue = this.getValueVariantMeta(variantIndex).initialValue;
525
+ return this.cloneDefaultValue(initialValue);
526
+ },
527
+ getSelectedValueVariant() {
528
+ if (Number.isInteger(this.selectedValueVariant)) {
529
+ return this.selectedValueVariant;
530
+ }
531
+ const detected = this.detectValueVariant(${fieldModelExpr});
532
+ this.selectedValueVariant = detected;
533
+ return detected;
534
+ },
535
+ selectValueVariant(variantIndex) {
536
+ this.selectedValueVariant = variantIndex;
537
+ this.ensureScopeObject();
538
+ ${fieldModelExpr} = this.cloneValueVariantInitialValue(variantIndex);
539
+ this.arrayValueKeys = [];
540
+ this.arrayObjectKeys = [];
541
+ this.selectedArrayItemVariants = {};
346
542
  },
347
543
  getSelectedArrayItemVariant(itemIndex) {
348
544
  if (Object.prototype.hasOwnProperty.call(this.selectedArrayItemVariants, itemIndex)) {
@@ -357,6 +553,7 @@ const layoutClass = isObjectLikeField
357
553
  this.selectedArrayItemVariants[itemIndex] = variantIndex;
358
554
  const target = this.ensureArrayObjectItem(itemIndex);
359
555
  this.pruneObjectForOneOf(target, variantIndex);
556
+ this.applyOneOfVariantDefaults(target, variantIndex, true);
360
557
  },
361
558
  shiftArrayItemVariantSelection(removedIndex) {
362
559
  const nextSelections = {};
@@ -405,9 +602,15 @@ const layoutClass = isObjectLikeField
405
602
  target[mapKey] = this.coerceScalarValue(value);
406
603
  this.setScalarValidity(element, value);
407
604
  },
605
+ isFileValue(value) {
606
+ return (
607
+ (typeof File !== 'undefined' && value instanceof File) ||
608
+ (typeof Blob !== 'undefined' && value instanceof Blob)
609
+ );
610
+ },
408
611
  coerceScalarValue(rawValue) {
612
+ if (this.isFileValue(rawValue)) return rawValue;
409
613
  const value = rawValue === undefined || rawValue === null ? '' : String(rawValue).trim();
410
- if (value === '__clear__') return '';
411
614
  if (value === '') return '';
412
615
  if (!${isNumericType}) return value;
413
616
 
@@ -422,8 +625,12 @@ const layoutClass = isObjectLikeField
422
625
  },
423
626
  setScalarValidity(element, rawValue) {
424
627
  if (!element || typeof element.setCustomValidity !== 'function') return;
628
+ if (this.isFileValue(rawValue)) {
629
+ element.setCustomValidity('');
630
+ return;
631
+ }
425
632
  const value = rawValue === undefined || rawValue === null ? '' : String(rawValue).trim();
426
- if (value === '' || value === '__clear__') {
633
+ if (value === '') {
427
634
  element.setCustomValidity('');
428
635
  return;
429
636
  }
@@ -467,11 +674,6 @@ const layoutClass = isObjectLikeField
467
674
 
468
675
  element.setCustomValidity('');
469
676
  },
470
- reportFieldValidity(element) {
471
- if (!element || typeof element.reportValidity !== 'function') return true;
472
- this.setScalarValidity(element, element.value);
473
- return element.reportValidity();
474
- },
475
677
  toBooleanOption(value) {
476
678
  if (value === true || value === 'true') return 'true';
477
679
  if (value === false || value === 'false') return 'false';
@@ -549,6 +751,51 @@ const layoutClass = isObjectLikeField
549
751
  // Keep previous valid value while editing invalid JSON.
550
752
  }
551
753
  },
754
+ getJsonFieldValue() {
755
+ const value = ${fieldModelExpr};
756
+ if (value === undefined || value === null || value === '') return '';
757
+ try {
758
+ return JSON.stringify(value, null, 2);
759
+ } catch (e) {
760
+ return '';
761
+ }
762
+ },
763
+ setJsonFieldValue(jsonValue, element) {
764
+ const raw = String(jsonValue || '');
765
+ this.ensureScopeObject();
766
+
767
+ if (!raw.trim()) {
768
+ delete ${resolvedScopeExpr}[${fieldNameKey}];
769
+ if (element && typeof element.setCustomValidity === 'function') {
770
+ element.setCustomValidity('');
771
+ }
772
+ return;
773
+ }
774
+
775
+ try {
776
+ const parsed = JSON.parse(raw);
777
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
778
+ if (element && typeof element.setCustomValidity === 'function') {
779
+ element.setCustomValidity('Enter a JSON object.');
780
+ }
781
+ return;
782
+ }
783
+
784
+ ${fieldModelExpr} = parsed;
785
+ if (element && typeof element.setCustomValidity === 'function') {
786
+ element.setCustomValidity('');
787
+ }
788
+ } catch (e) {
789
+ if (element && typeof element.setCustomValidity === 'function') {
790
+ element.setCustomValidity('Enter valid JSON.');
791
+ }
792
+ }
793
+ },
794
+ formatJsonFieldValue(element) {
795
+ if (!element || typeof element.value !== 'string') return;
796
+ if (element.validationMessage) return;
797
+ element.value = this.getJsonFieldValue();
798
+ },
552
799
  getFieldValue() {
553
800
  const value = ${fieldModelExpr};
554
801
  return value === undefined || value === null ? '' : value;
@@ -558,6 +805,10 @@ const layoutClass = isObjectLikeField
558
805
  ${fieldModelExpr} = this.coerceScalarValue(value);
559
806
  this.setScalarValidity(element, value);
560
807
  },
808
+ clearFieldValue() {
809
+ this.ensureScopeObject();
810
+ delete ${resolvedScopeExpr}[${fieldNameKey}];
811
+ },
561
812
  ensureArrayField() {
562
813
  this.ensureScopeObject();
563
814
  if (!Array.isArray(${fieldModelExpr})) {
@@ -567,7 +818,7 @@ const layoutClass = isObjectLikeField
567
818
  },
568
819
  getArrayValues() {
569
820
  const values = ${fieldModelExpr};
570
- return Array.isArray(values) && values.length > 0 ? values : [''];
821
+ return Array.isArray(values) ? values : [''];
571
822
  },
572
823
  setArrayValue(index, value, element) {
573
824
  const values = this.ensureArrayField();
@@ -575,20 +826,34 @@ const layoutClass = isObjectLikeField
575
826
  this.setScalarValidity(element, value);
576
827
  },
577
828
  addArrayValue() {
829
+ const hadArray = Array.isArray(${fieldModelExpr});
578
830
  const values = this.ensureArrayField();
579
- if (values.length === 0) {
831
+ if (!hadArray) {
832
+ this.arrayValueKeys = [];
833
+ }
834
+ if (values.length === 0 && !hadArray) {
580
835
  values.push('', '');
836
+ this.syncArrayKeys(this.arrayValueKeys, values.length);
581
837
  return;
582
838
  }
583
839
  values.push('');
840
+ this.syncArrayKeys(this.arrayValueKeys, values.length);
584
841
  },
585
842
  removeArrayValue(index) {
586
843
  const values = ${fieldModelExpr};
587
- if (!Array.isArray(values)) return;
844
+ if (!Array.isArray(values)) {
845
+ this.ensureScopeObject();
846
+ ${fieldModelExpr} = [];
847
+ this.arrayValueKeys = [];
848
+ return;
849
+ }
588
850
  values.splice(index, 1);
851
+ this.arrayValueKeys.splice(index, 1);
589
852
  if (values.length === 0) {
590
- delete ${resolvedScopeExpr}[${fieldNameKey}];
853
+ this.ensureScopeObject();
854
+ ${fieldModelExpr} = [];
591
855
  }
856
+ this.syncArrayKeys(this.arrayValueKeys, values.length);
592
857
  },
593
858
  getArrayObjectValues() {
594
859
  const values = ${fieldModelExpr};
@@ -608,15 +873,19 @@ const layoutClass = isObjectLikeField
608
873
  addArrayObjectValue() {
609
874
  const values = this.ensureArrayField();
610
875
  values.push({});
876
+ this.syncArrayKeys(this.arrayObjectKeys, values.length);
611
877
  },
612
878
  removeArrayObjectValue(index) {
613
879
  const values = ${fieldModelExpr};
614
880
  if (!Array.isArray(values)) return;
615
881
  values.splice(index, 1);
882
+ this.arrayObjectKeys.splice(index, 1);
616
883
  this.shiftArrayItemVariantSelection(index);
617
884
  if (values.length === 0) {
618
- delete ${resolvedScopeExpr}[${fieldNameKey}];
885
+ this.ensureScopeObject();
886
+ ${fieldModelExpr} = [];
619
887
  }
888
+ this.syncArrayKeys(this.arrayObjectKeys, values.length);
620
889
  }
621
890
  }`}
622
891
  x-effect="applyInitialDefault()"
@@ -638,7 +907,56 @@ const layoutClass = isObjectLikeField
638
907
  </div>
639
908
  <div class="w-full">
640
909
  {
641
- isAdditionalPropertyField ? (
910
+ hasValueVariants ? (
911
+ <div class="rounded-lg border border-neutral-200 bg-neutral-50/50 p-2.5 dark:border-neutral-800 dark:bg-neutral-900/50">
912
+ <div class="space-y-3">
913
+ <div class="space-y-1.5">
914
+ <p class="text-xs text-neutral-500 dark:text-neutral-400">
915
+ Select value type.
916
+ </p>
917
+ <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">
918
+ {valueVariants.map((variant, variantIndex) => (
919
+ <button
920
+ type="button"
921
+ @click={`selectValueVariant(${variantIndex})`}
922
+ :class={`{
923
+ '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': getSelectedValueVariant() === ${variantIndex},
924
+ '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': getSelectedValueVariant() !== ${variantIndex}
925
+ }`}
926
+ 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"
927
+ >
928
+ {variant.label}
929
+ </button>
930
+ ))}
931
+ </div>
932
+ </div>
933
+ {valueVariants.map((variant, variantIndex) => (
934
+ <template x-if={`getSelectedValueVariant() === ${variantIndex}`}>
935
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5 dark:border-neutral-800 dark:bg-neutral-900/60">
936
+ <Astro.self
937
+ field={variant.field}
938
+ requestPart={requestPart}
939
+ scopeExpr={resolvedScopeExpr}
940
+ defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
941
+ requiredEnabledExpr={resolvedRequiredEnabledExpr}
942
+ />
943
+ </div>
944
+ </template>
945
+ ))}
946
+ </div>
947
+ </div>
948
+ ) : renderAsJsonInput ? (
949
+ <textarea
950
+ name={`${requestPart}_${field.name}`}
951
+ rows="8"
952
+ :value="getJsonFieldValue()"
953
+ @input="setJsonFieldValue($event.target.value, $event.target)"
954
+ @blur="formatJsonFieldValue($event.target)"
955
+ :required={fieldRequiredExpr}
956
+ class="w-full px-3 py-2 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow placeholder:text-neutral-400 transition-all duration-200 bg-white font-mono dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:placeholder:text-neutral-500 dark:focus:border-neutral-600"
957
+ placeholder={`{\n "type": "object",\n "properties": {}\n}`}
958
+ />
959
+ ) : isAdditionalPropertyField ? (
642
960
  <div class="space-y-2">
643
961
  <template
644
962
  x-for={`(mapKey, mapIndex) in getMapEntryKeys()`}
@@ -680,7 +998,6 @@ const layoutClass = isObjectLikeField
680
998
  class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
681
999
  >
682
1000
  <option value="" disabled hidden>Select value</option>
683
- <option value="__clear__">Clear selection</option>
684
1001
  {field.enum.map((value) => (
685
1002
  <option value={String(value)}>{value}</option>
686
1003
  ))}
@@ -699,7 +1016,6 @@ const layoutClass = isObjectLikeField
699
1016
  class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
700
1017
  >
701
1018
  <option value="" disabled hidden>Select value</option>
702
- <option value="__clear__">Clear selection</option>
703
1019
  <option value="true">true</option>
704
1020
  <option value="false">false</option>
705
1021
  </select>
@@ -713,7 +1029,7 @@ const layoutClass = isObjectLikeField
713
1029
  type={inputType}
714
1030
  :value="getMapValue(mapKey)"
715
1031
  @input="setMapValue(mapKey, $event.target.value, $event.target)"
716
- @blur="reportFieldValidity($event.target)"
1032
+ @blur="setScalarValidity($event.target, $event.target.value)"
717
1033
  min={numericInputMin}
718
1034
  max={numericInputMax}
719
1035
  step={numericInputStep}
@@ -740,10 +1056,10 @@ const layoutClass = isObjectLikeField
740
1056
  <div class="space-y-2">
741
1057
  <template
742
1058
  x-for={`(item, index) in getArrayObjectValues()`}
743
- :key={`${fieldNameKey} + '-item-' + index`}
1059
+ :key={`getArrayObjectKey(index)`}
744
1060
  >
745
1061
  <div
746
- class="rounded-lg border border-neutral-200 bg-neutral-50/50 p-2.5 dark:border-neutral-800 dark:bg-neutral-900/50"
1062
+ class="overflow-hidden rounded-xl border-[0.5px] border-neutral-900/12 bg-neutral-50/50 p-3 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)]"
747
1063
  x-init="ensureArrayObjectItem(index)"
748
1064
  >
749
1065
  <div class="mb-2 flex items-center justify-between">
@@ -765,8 +1081,9 @@ const layoutClass = isObjectLikeField
765
1081
  <Astro.self
766
1082
  field={nestedField}
767
1083
  requestPart={requestPart}
768
- scopeExpr={`${fieldModelExpr}[index]`}
1084
+ scopeExpr="item"
769
1085
  defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
1086
+ requiredEnabledExpr={resolvedRequiredEnabledExpr}
770
1087
  />
771
1088
  </div>
772
1089
  ))}
@@ -807,8 +1124,9 @@ const layoutClass = isObjectLikeField
807
1124
  <Astro.self
808
1125
  field={variantField}
809
1126
  requestPart={requestPart}
810
- scopeExpr={`${fieldModelExpr}[index]`}
1127
+ scopeExpr="item"
811
1128
  defaultsEnabledExpr={`(${resolvedDefaultsEnabledExpr}) && getSelectedArrayItemVariant(index) === ${variantIndex}`}
1129
+ requiredEnabledExpr={`(${resolvedRequiredEnabledExpr}) && getSelectedArrayItemVariant(index) === ${variantIndex}`}
812
1130
  />
813
1131
  </div>
814
1132
  ))}
@@ -833,8 +1151,9 @@ const layoutClass = isObjectLikeField
833
1151
  <Astro.self
834
1152
  field={variantField}
835
1153
  requestPart={requestPart}
836
- scopeExpr={`${fieldModelExpr}[index]`}
1154
+ scopeExpr="item"
837
1155
  defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
1156
+ requiredEnabledExpr={resolvedRequiredEnabledExpr}
838
1157
  />
839
1158
  </div>
840
1159
  ))}
@@ -859,7 +1178,7 @@ const layoutClass = isObjectLikeField
859
1178
  </div>
860
1179
  ) : (
861
1180
  <div class="space-y-2">
862
- <template x-for={`(value, index) in getArrayValues()`} :key={`${fieldNameKey} + '-' + index`}>
1181
+ <template x-for={`(value, index) in getArrayValues()`} :key={`getArrayValueKey(index)`}>
863
1182
  <div class="flex items-center gap-2">
864
1183
  {
865
1184
  field.enum && field.enum.length > 0 ? (
@@ -868,12 +1187,11 @@ const layoutClass = isObjectLikeField
868
1187
  name={`${requestPart}_${field.name}`}
869
1188
  :value="value"
870
1189
  @change={`setArrayValue(index, $event.target.value, $event.target)`}
871
- :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
1190
+ :required={`index === 0 && (${fieldRequiredExpr})`}
872
1191
  :class="value ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
873
1192
  class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
874
1193
  >
875
1194
  <option value="" disabled hidden>Select value</option>
876
- <option value="__clear__">Clear selection</option>
877
1195
  {field.enum.map((value) => (
878
1196
  <option value={String(value)}>{value}</option>
879
1197
  ))}
@@ -890,12 +1208,11 @@ const layoutClass = isObjectLikeField
890
1208
  name={`${requestPart}_${field.name}`}
891
1209
  :value="getArrayBooleanValue(index)"
892
1210
  @change={`setArrayBooleanValue(index, $event.target.value)`}
893
- :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
1211
+ :required={`index === 0 && (${fieldRequiredExpr})`}
894
1212
  :class="getArrayBooleanValue(index) ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
895
1213
  class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
896
1214
  >
897
1215
  <option value="" disabled hidden>Select value</option>
898
- <option value="__clear__">Clear selection</option>
899
1216
  <option value="true">true</option>
900
1217
  <option value="false">false</option>
901
1218
  </select>
@@ -904,15 +1221,23 @@ const layoutClass = isObjectLikeField
904
1221
  name="lucide:chevrons-up-down"
905
1222
  />
906
1223
  </div>
1224
+ ) : isFileInput ? (
1225
+ <input
1226
+ name={field.name}
1227
+ type="file"
1228
+ @change={`setArrayValue(index, $event.target.files?.[0] || '', $event.target)`}
1229
+ :required={`index === 0 && (${fieldRequiredExpr})`}
1230
+ class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow file:mr-3 file:rounded-md file:border-0 file:bg-neutral-100 file:px-2.5 file:py-1 file:text-xs file:font-medium file:text-neutral-700 transition-all duration-200 bg-white dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:file:bg-neutral-800 dark:file:text-neutral-200 dark:focus:border-neutral-600"
1231
+ />
907
1232
  ) : (
908
1233
  <input
909
1234
  name={field.name}
910
- placeholder={`Enter ${field.name}`}
1235
+ placeholder={fieldPlaceholder}
911
1236
  type={inputType}
912
1237
  :value="value"
913
1238
  @input={`setArrayValue(index, $event.target.value, $event.target)`}
914
- @blur="reportFieldValidity($event.target)"
915
- :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
1239
+ @blur="setScalarValidity($event.target, $event.target.value)"
1240
+ :required={`index === 0 && (${fieldRequiredExpr})`}
916
1241
  min={numericInputMin}
917
1242
  max={numericInputMax}
918
1243
  step={numericInputStep}
@@ -953,6 +1278,7 @@ const layoutClass = isObjectLikeField
953
1278
  requestPart={requestPart}
954
1279
  scopeExpr={fieldModelExpr}
955
1280
  defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
1281
+ requiredEnabledExpr={childRequiredEnabledExpr}
956
1282
  />
957
1283
  </div>
958
1284
  ))}
@@ -995,6 +1321,7 @@ const layoutClass = isObjectLikeField
995
1321
  requestPart={requestPart}
996
1322
  scopeExpr={fieldModelExpr}
997
1323
  defaultsEnabledExpr={`(${resolvedDefaultsEnabledExpr}) && getSelectedOneOfVariant() === ${variantIndex}`}
1324
+ requiredEnabledExpr={`(${childRequiredEnabledExpr}) && getSelectedOneOfVariant() === ${variantIndex}`}
998
1325
  />
999
1326
  </div>
1000
1327
  ))}
@@ -1021,6 +1348,7 @@ const layoutClass = isObjectLikeField
1021
1348
  requestPart={requestPart}
1022
1349
  scopeExpr={fieldModelExpr}
1023
1350
  defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
1351
+ requiredEnabledExpr={childRequiredEnabledExpr}
1024
1352
  />
1025
1353
  </div>
1026
1354
  ))}
@@ -1036,55 +1364,89 @@ const layoutClass = isObjectLikeField
1036
1364
  ) : (
1037
1365
  <>
1038
1366
  {field.enum && field.enum.length > 0 ? (
1039
- <div class="relative">
1040
- <select
1041
- name={`${requestPart}_${field.name}`}
1042
- :value="getFieldValue()"
1043
- @change="setFieldValue($event.target.value, $event.target)"
1044
- required={isRequiredField}
1045
- :class="getFieldValue() ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
1046
- class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
1047
- >
1048
- <option value="" disabled hidden>Select value</option>
1049
- <option value="__clear__">Clear selection</option>
1050
- {field.enum.map((value) => (
1051
- <option value={String(value)}>{value}</option>
1052
- ))}
1053
- </select>
1054
- <Icon
1055
- class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
1056
- name="lucide:chevrons-up-down"
1057
- />
1367
+ <div class="flex items-center gap-2">
1368
+ <div class="relative w-full">
1369
+ <select
1370
+ name={`${requestPart}_${field.name}`}
1371
+ :value="getFieldValue()"
1372
+ @change="setFieldValue($event.target.value, $event.target)"
1373
+ :required={fieldRequiredExpr}
1374
+ :class="getFieldValue() ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
1375
+ class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
1376
+ >
1377
+ <option value="" disabled hidden>Select value</option>
1378
+ {field.enum.map((value) => (
1379
+ <option value={String(value)}>{value}</option>
1380
+ ))}
1381
+ </select>
1382
+ <Icon
1383
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
1384
+ name="lucide:chevrons-up-down"
1385
+ />
1386
+ </div>
1387
+ {!isRequiredField && (
1388
+ <button
1389
+ type="button"
1390
+ @click="clearFieldValue()"
1391
+ x-show="getFieldValue()"
1392
+ x-cloak
1393
+ class="h-[31px] px-2 rounded-md border border-neutral-200 text-neutral-500 hover:text-neutral-700 hover:border-neutral-300 hover:bg-neutral-50 transition-colors duration-200 cursor-pointer dark:border-neutral-700 dark:text-neutral-400 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/70 dark:hover:text-neutral-200"
1394
+ aria-label={`Clear ${field.name}`}
1395
+ >
1396
+ <Icon class="size-4" name="lucide:x" />
1397
+ </button>
1398
+ )}
1058
1399
  </div>
1059
1400
  ) : isBooleanType ? (
1060
- <div class="relative">
1061
- <select
1062
- name={`${requestPart}_${field.name}`}
1063
- :value="getBooleanFieldValue()"
1064
- @change="setBooleanFieldValue($event.target.value)"
1065
- required={isRequiredField}
1066
- :class="getBooleanFieldValue() ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
1067
- class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
1068
- >
1069
- <option value="" disabled hidden>Select value</option>
1070
- <option value="__clear__">Clear selection</option>
1071
- <option value="true">true</option>
1072
- <option value="false">false</option>
1073
- </select>
1074
- <Icon
1075
- class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
1076
- name="lucide:chevrons-up-down"
1077
- />
1401
+ <div class="flex items-center gap-2">
1402
+ <div class="relative w-full">
1403
+ <select
1404
+ name={`${requestPart}_${field.name}`}
1405
+ :value="getBooleanFieldValue()"
1406
+ @change="setBooleanFieldValue($event.target.value)"
1407
+ :required={fieldRequiredExpr}
1408
+ :class="getBooleanFieldValue() ? 'text-neutral-700 dark:text-neutral-200' : 'text-neutral-400 dark:text-neutral-500'"
1409
+ class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow transition-all duration-200 bg-white appearance-none dark:border-neutral-700 dark:bg-neutral-900 dark:focus:border-neutral-600"
1410
+ >
1411
+ <option value="" disabled hidden>Select value</option>
1412
+ <option value="true">true</option>
1413
+ <option value="false">false</option>
1414
+ </select>
1415
+ <Icon
1416
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
1417
+ name="lucide:chevrons-up-down"
1418
+ />
1419
+ </div>
1420
+ {!isRequiredField && (
1421
+ <button
1422
+ type="button"
1423
+ @click="clearFieldValue()"
1424
+ x-show="getBooleanFieldValue()"
1425
+ x-cloak
1426
+ class="h-[31px] px-2 rounded-md border border-neutral-200 text-neutral-500 hover:text-neutral-700 hover:border-neutral-300 hover:bg-neutral-50 transition-colors duration-200 cursor-pointer dark:border-neutral-700 dark:text-neutral-400 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/70 dark:hover:text-neutral-200"
1427
+ aria-label={`Clear ${field.name}`}
1428
+ >
1429
+ <Icon class="size-4" name="lucide:x" />
1430
+ </button>
1431
+ )}
1078
1432
  </div>
1433
+ ) : isFileInput ? (
1434
+ <input
1435
+ name={field.name}
1436
+ type="file"
1437
+ @change="setFieldValue($event.target.files?.[0] || '', $event.target)"
1438
+ :required={fieldRequiredExpr}
1439
+ class="w-full px-3 py-1.5 text-sm border border-neutral-200 focus:border-neutral-300 outline-none rounded-md shadow-xs focus:shadow file:mr-3 file:rounded-md file:border-0 file:bg-neutral-100 file:px-2.5 file:py-1 file:text-xs file:font-medium file:text-neutral-700 transition-all duration-200 bg-white dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200 dark:file:bg-neutral-800 dark:file:text-neutral-200 dark:focus:border-neutral-600"
1440
+ />
1079
1441
  ) : (
1080
1442
  <input
1081
1443
  name={field.name}
1082
- placeholder={`Enter ${field.name}`}
1444
+ placeholder={fieldPlaceholder}
1083
1445
  type={inputType}
1084
1446
  :value="getFieldValue()"
1085
1447
  @input="setFieldValue($event.target.value, $event.target)"
1086
- @blur="reportFieldValidity($event.target)"
1087
- required={isRequiredField}
1448
+ @blur="setScalarValidity($event.target, $event.target.value)"
1449
+ :required={fieldRequiredExpr}
1088
1450
  min={numericInputMin}
1089
1451
  max={numericInputMax}
1090
1452
  step={numericInputStep}