radiant-docs 0.1.7 → 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 +28 -5
  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
@@ -6,13 +6,621 @@ import Field from "../ui/Field.astro";
6
6
  interface Props {
7
7
  field: FieldType;
8
8
  requestPart: string;
9
+ scopeExpr?: string;
10
+ defaultsEnabledExpr?: string;
9
11
  }
10
12
 
11
- const { field, requestPart } = Astro.props;
13
+ const { field, requestPart, scopeExpr, defaultsEnabledExpr } = Astro.props;
14
+
15
+ function cloneDefaultTemplate<T>(value: T): T {
16
+ if (value === undefined) return value;
17
+ try {
18
+ return JSON.parse(JSON.stringify(value)) as T;
19
+ } catch {
20
+ return value;
21
+ }
22
+ }
23
+
24
+ function buildObjectDefaultTemplateFromFields(
25
+ fields: FieldType[] | undefined,
26
+ ): Record<string, unknown> {
27
+ const defaults: Record<string, unknown> = {};
28
+ if (!fields || fields.length === 0) return defaults;
29
+
30
+ fields.forEach((childField) => {
31
+ if (childField.isAdditionalProperty) return;
32
+ const childDefault = buildFieldDefaultTemplate(childField);
33
+ if (childDefault !== undefined) {
34
+ defaults[childField.name] = childDefault;
35
+ }
36
+ });
37
+
38
+ return defaults;
39
+ }
40
+
41
+ function mergeObjectDefaults(
42
+ target: Record<string, unknown>,
43
+ source: Record<string, unknown>,
44
+ ) {
45
+ Object.entries(source).forEach(([key, value]) => {
46
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
47
+ target[key] = value;
48
+ }
49
+ });
50
+ }
51
+
52
+ function buildVariantDefaultTemplate(field: FieldType): Record<string, unknown> {
53
+ const variants = field.variants || [];
54
+ if (variants.length === 0) return {};
55
+
56
+ if (field.variantType === "oneOf") {
57
+ for (const variant of variants) {
58
+ const variantDefaults = buildObjectDefaultTemplateFromFields(
59
+ variant.fields,
60
+ );
61
+ if (Object.keys(variantDefaults).length > 0) {
62
+ return variantDefaults;
63
+ }
64
+ }
65
+ return {};
66
+ }
67
+
68
+ const merged: Record<string, unknown> = {};
69
+ variants.forEach((variant) => {
70
+ const variantDefaults = buildObjectDefaultTemplateFromFields(variant.fields);
71
+ mergeObjectDefaults(merged, variantDefaults);
72
+ });
73
+
74
+ return merged;
75
+ }
76
+
77
+ function buildFieldDefaultTemplate(field: FieldType): unknown | undefined {
78
+ if (field.hasDefault) {
79
+ return cloneDefaultTemplate(field.defaultValue);
80
+ }
81
+
82
+ const nestedDefaults = buildObjectDefaultTemplateFromFields(field.nested);
83
+ const variantDefaults = buildVariantDefaultTemplate(field);
84
+
85
+ if (field.isArray) {
86
+ const itemDefaults: Record<string, unknown> = {};
87
+ mergeObjectDefaults(itemDefaults, nestedDefaults);
88
+ mergeObjectDefaults(itemDefaults, variantDefaults);
89
+
90
+ if (field.required && Object.keys(itemDefaults).length > 0) {
91
+ return [itemDefaults];
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ const isObjectLikeField =
97
+ (field.nested && field.nested.length > 0) ||
98
+ (field.variants && field.variants.length > 0) ||
99
+ /\bobject\b/.test(field.type);
100
+ if (!isObjectLikeField) return undefined;
101
+
102
+ const objectDefaults: Record<string, unknown> = {};
103
+ mergeObjectDefaults(objectDefaults, nestedDefaults);
104
+ mergeObjectDefaults(objectDefaults, variantDefaults);
105
+
106
+ if (!field.required) return undefined;
107
+ return Object.keys(objectDefaults).length > 0 ? objectDefaults : undefined;
108
+ }
109
+
110
+ const requestPartKey = JSON.stringify(requestPart);
111
+ const fieldNameKey = JSON.stringify(field.name);
112
+ const defaultScopeExpr = `inputs[${requestPartKey}]`;
113
+ const resolvedScopeExpr = scopeExpr || defaultScopeExpr;
114
+ const resolvedDefaultsEnabledExpr = defaultsEnabledExpr || "true";
115
+ const fieldModelExpr = `${resolvedScopeExpr}[${fieldNameKey}]`;
116
+ const isNumericType = /\b(number|integer)\b/.test(field.type);
117
+ const isIntegerType = /\binteger\b/.test(field.type);
118
+ const hasStringType = /\bstring\b/.test(field.type);
119
+ const isBooleanType = /\bboolean\b/.test(field.type);
120
+ const inputType = isNumericType && !hasStringType ? "number" : "text";
121
+ const isRequiredField = field.required === true;
122
+ const stringMinLength =
123
+ typeof field.minLength === "number" &&
124
+ Number.isInteger(field.minLength) &&
125
+ field.minLength >= 0
126
+ ? field.minLength
127
+ : undefined;
128
+ const stringMaxLength =
129
+ typeof field.maxLength === "number" &&
130
+ Number.isInteger(field.maxLength) &&
131
+ field.maxLength >= 0
132
+ ? field.maxLength
133
+ : undefined;
134
+ const numericMinimum =
135
+ typeof field.minimum === "number" && Number.isFinite(field.minimum)
136
+ ? field.minimum
137
+ : undefined;
138
+ const numericMaximum =
139
+ typeof field.maximum === "number" && Number.isFinite(field.maximum)
140
+ ? field.maximum
141
+ : undefined;
142
+ const numericExclusiveMinimum =
143
+ typeof field.exclusiveMinimum === "number" &&
144
+ Number.isFinite(field.exclusiveMinimum)
145
+ ? field.exclusiveMinimum
146
+ : undefined;
147
+ const numericExclusiveMaximum =
148
+ typeof field.exclusiveMaximum === "number" &&
149
+ Number.isFinite(field.exclusiveMaximum)
150
+ ? field.exclusiveMaximum
151
+ : undefined;
152
+ const numericInputMin =
153
+ inputType === "number"
154
+ ? numericMinimum !== undefined
155
+ ? numericMinimum
156
+ : isIntegerType && numericExclusiveMinimum !== undefined
157
+ ? Math.floor(numericExclusiveMinimum) + 1
158
+ : numericExclusiveMinimum
159
+ : undefined;
160
+ const numericInputMax =
161
+ inputType === "number"
162
+ ? numericMaximum !== undefined
163
+ ? numericMaximum
164
+ : isIntegerType && numericExclusiveMaximum !== undefined
165
+ ? Math.ceil(numericExclusiveMaximum) - 1
166
+ : numericExclusiveMaximum
167
+ : undefined;
168
+ const numericInputStep =
169
+ inputType === "number" ? (isIntegerType ? "1" : "any") : undefined;
170
+ const textInputMinLength = inputType === "text" ? stringMinLength : undefined;
171
+ const textInputMaxLength = inputType === "text" ? stringMaxLength : undefined;
172
+ const hasNumericMinimum = numericMinimum !== undefined;
173
+ const hasNumericMaximum = numericMaximum !== undefined;
174
+ const hasNumericExclusiveMinimum = numericExclusiveMinimum !== undefined;
175
+ const hasNumericExclusiveMaximum = numericExclusiveMaximum !== undefined;
176
+ const hasStringMinLength = stringMinLength !== undefined;
177
+ const hasStringMaxLength = stringMaxLength !== undefined;
178
+ const numericMinimumLiteral =
179
+ numericMinimum !== undefined ? JSON.stringify(numericMinimum) : "undefined";
180
+ const numericMaximumLiteral =
181
+ numericMaximum !== undefined ? JSON.stringify(numericMaximum) : "undefined";
182
+ const numericExclusiveMinimumLiteral =
183
+ numericExclusiveMinimum !== undefined
184
+ ? JSON.stringify(numericExclusiveMinimum)
185
+ : "undefined";
186
+ const numericExclusiveMaximumLiteral =
187
+ numericExclusiveMaximum !== undefined
188
+ ? JSON.stringify(numericExclusiveMaximum)
189
+ : "undefined";
190
+ const stringMinLengthLiteral =
191
+ stringMinLength !== undefined ? JSON.stringify(stringMinLength) : "undefined";
192
+ const stringMaxLengthLiteral =
193
+ stringMaxLength !== undefined ? JSON.stringify(stringMaxLength) : "undefined";
194
+ const hasNestedFields = Boolean(field.nested && field.nested.length > 0);
195
+ const hasFieldVariants = Boolean(field.variants && field.variants.length > 0);
196
+ const hasOneOfVariants = field.variantType === "oneOf" && hasFieldVariants;
197
+ const oneOfVariantFieldKeys = hasOneOfVariants
198
+ ? (field.variants || []).map((variant) =>
199
+ (variant.fields || []).map((variantField) => variantField.name),
200
+ )
201
+ : [];
202
+ const isAdditionalPropertyField = field.isAdditionalProperty === true;
203
+ const mapKnownKeys = JSON.stringify(field.mapKnownKeys || []);
204
+ const oneOfVariantFieldKeysJson = JSON.stringify(oneOfVariantFieldKeys);
205
+ const defaultTemplate = buildFieldDefaultTemplate(field);
206
+ const hasDefaultTemplate = defaultTemplate !== undefined;
207
+ const defaultTemplateJson = JSON.stringify(defaultTemplate ?? null).replaceAll(
208
+ "`",
209
+ "\\`",
210
+ );
211
+ const hasComplexMapValue =
212
+ field.isArray ||
213
+ hasNestedFields ||
214
+ hasFieldVariants ||
215
+ /\bobject\b/.test(field.type);
216
+ const isObjectLikeField =
217
+ isAdditionalPropertyField ||
218
+ hasNestedFields ||
219
+ hasFieldVariants ||
220
+ /\bobject\b/.test(field.type);
221
+ const layoutClass = isObjectLikeField
222
+ ? "flex justify-between flex-col gap-4"
223
+ : "flex justify-between flex-col sm:flex-row lg:flex-col xl:flex-row gap-4";
12
224
  ---
13
225
 
14
226
  <div
15
- class="flex justify-between flex-col sm:flex-row lg:flex-col xl:flex-row gap-4"
227
+ x-data={`{
228
+ hasDefaultTemplate: ${hasDefaultTemplate ? "true" : "false"},
229
+ defaultTemplate: ${defaultTemplateJson},
230
+ ensureScopeObject() {
231
+ if (
232
+ !${resolvedScopeExpr} ||
233
+ typeof ${resolvedScopeExpr} !== 'object' ||
234
+ Array.isArray(${resolvedScopeExpr})
235
+ ) {
236
+ ${resolvedScopeExpr} = {};
237
+ }
238
+ return ${resolvedScopeExpr};
239
+ },
240
+ cloneDefaultValue(value) {
241
+ if (value === undefined) return undefined;
242
+ try {
243
+ return JSON.parse(JSON.stringify(value));
244
+ } catch (e) {
245
+ return value;
246
+ }
247
+ },
248
+ defaultsAreEnabled() {
249
+ try {
250
+ return Boolean(${resolvedDefaultsEnabledExpr});
251
+ } catch (e) {
252
+ return false;
253
+ }
254
+ },
255
+ applyInitialDefault() {
256
+ if (!this.hasDefaultTemplate || ${isAdditionalPropertyField ? "true" : "false"}) return;
257
+ if (!this.defaultsAreEnabled()) return;
258
+
259
+ this.ensureScopeObject();
260
+ if (Object.prototype.hasOwnProperty.call(${resolvedScopeExpr}, ${fieldNameKey})) {
261
+ return;
262
+ }
263
+
264
+ const nextValue = this.cloneDefaultValue(this.defaultTemplate);
265
+ if (nextValue === undefined) return;
266
+
267
+ ${fieldModelExpr} = nextValue;
268
+
269
+ if (
270
+ ${hasOneOfVariants ? "true" : "false"} &&
271
+ nextValue &&
272
+ typeof nextValue === 'object' &&
273
+ !Array.isArray(nextValue)
274
+ ) {
275
+ this.selectedOneOfVariant = this.detectOneOfVariant(nextValue);
276
+ }
277
+ },
278
+ init() {
279
+ this.applyInitialDefault();
280
+ },
281
+ oneOfVariantFieldKeys: ${oneOfVariantFieldKeysJson},
282
+ selectedOneOfVariant: null,
283
+ selectedArrayItemVariants: {},
284
+ ensureFieldObject() {
285
+ this.ensureScopeObject();
286
+ if (
287
+ !${fieldModelExpr} ||
288
+ typeof ${fieldModelExpr} !== 'object' ||
289
+ Array.isArray(${fieldModelExpr})
290
+ ) {
291
+ ${fieldModelExpr} = {};
292
+ }
293
+ return ${fieldModelExpr};
294
+ },
295
+ getOneOfVariantFieldKeys(variantIndex) {
296
+ const keys = this.oneOfVariantFieldKeys[variantIndex];
297
+ return Array.isArray(keys) ? keys : [];
298
+ },
299
+ detectOneOfVariant(target) {
300
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
301
+ return 0;
302
+ }
303
+ let selectedIndex = 0;
304
+ let bestScore = -1;
305
+ this.oneOfVariantFieldKeys.forEach((keys, variantIndex) => {
306
+ if (!Array.isArray(keys) || keys.length === 0) return;
307
+ const score = keys.reduce((total, key) => {
308
+ return total + (Object.prototype.hasOwnProperty.call(target, key) ? 1 : 0);
309
+ }, 0);
310
+ if (score > bestScore) {
311
+ bestScore = score;
312
+ selectedIndex = variantIndex;
313
+ }
314
+ });
315
+ return selectedIndex;
316
+ },
317
+ pruneObjectForOneOf(target, selectedVariantIndex) {
318
+ if (!target || typeof target !== 'object' || Array.isArray(target)) return;
319
+ const selectedKeys = new Set(
320
+ this.getOneOfVariantFieldKeys(selectedVariantIndex),
321
+ );
322
+ const keysToRemove = new Set();
323
+ this.oneOfVariantFieldKeys.forEach((keys, variantIndex) => {
324
+ if (!Array.isArray(keys) || variantIndex === selectedVariantIndex) return;
325
+ keys.forEach((key) => {
326
+ if (!selectedKeys.has(key)) keysToRemove.add(key);
327
+ });
328
+ });
329
+ keysToRemove.forEach((key) => {
330
+ delete target[key];
331
+ });
332
+ },
333
+ getSelectedOneOfVariant() {
334
+ if (Number.isInteger(this.selectedOneOfVariant)) {
335
+ return this.selectedOneOfVariant;
336
+ }
337
+ const target = ${fieldModelExpr};
338
+ const detected = this.detectOneOfVariant(target);
339
+ this.selectedOneOfVariant = detected;
340
+ return detected;
341
+ },
342
+ selectOneOfVariant(variantIndex) {
343
+ this.selectedOneOfVariant = variantIndex;
344
+ const target = this.ensureFieldObject();
345
+ this.pruneObjectForOneOf(target, variantIndex);
346
+ },
347
+ getSelectedArrayItemVariant(itemIndex) {
348
+ if (Object.prototype.hasOwnProperty.call(this.selectedArrayItemVariants, itemIndex)) {
349
+ return this.selectedArrayItemVariants[itemIndex];
350
+ }
351
+ const values = this.getArrayObjectValues();
352
+ const detected = this.detectOneOfVariant(values[itemIndex]);
353
+ this.selectedArrayItemVariants[itemIndex] = detected;
354
+ return detected;
355
+ },
356
+ selectArrayItemVariant(itemIndex, variantIndex) {
357
+ this.selectedArrayItemVariants[itemIndex] = variantIndex;
358
+ const target = this.ensureArrayObjectItem(itemIndex);
359
+ this.pruneObjectForOneOf(target, variantIndex);
360
+ },
361
+ shiftArrayItemVariantSelection(removedIndex) {
362
+ const nextSelections = {};
363
+ Object.entries(this.selectedArrayItemVariants).forEach(([rawIndex, variantIndex]) => {
364
+ const index = Number(rawIndex);
365
+ if (!Number.isInteger(index) || index === removedIndex) return;
366
+ nextSelections[index > removedIndex ? index - 1 : index] = variantIndex;
367
+ });
368
+ this.selectedArrayItemVariants = nextSelections;
369
+ },
370
+ mapKnownKeys: ${mapKnownKeys},
371
+ getMapEntryKeys() {
372
+ const target = this.ensureScopeObject();
373
+ return Object.keys(target).filter((key) => !this.mapKnownKeys.includes(key));
374
+ },
375
+ addMapEntry() {
376
+ const target = this.ensureScopeObject();
377
+ const baseKey = 'key';
378
+ let nextKey = baseKey;
379
+ let index = 1;
380
+ while (Object.prototype.hasOwnProperty.call(target, nextKey)) {
381
+ nextKey = baseKey + '_' + index;
382
+ index += 1;
383
+ }
384
+ target[nextKey] = '';
385
+ },
386
+ removeMapEntry(mapKey) {
387
+ const target = this.ensureScopeObject();
388
+ delete target[mapKey];
389
+ },
390
+ renameMapKey(oldKey, newKey) {
391
+ const target = this.ensureScopeObject();
392
+ const nextKey = String(newKey || '').trim();
393
+ if (!nextKey || nextKey === oldKey) return;
394
+ if (Object.prototype.hasOwnProperty.call(target, nextKey)) return;
395
+ target[nextKey] = target[oldKey];
396
+ delete target[oldKey];
397
+ },
398
+ getMapValue(mapKey) {
399
+ const target = this.ensureScopeObject();
400
+ const value = target[mapKey];
401
+ return value === undefined || value === null ? '' : value;
402
+ },
403
+ setMapValue(mapKey, value, element) {
404
+ const target = this.ensureScopeObject();
405
+ target[mapKey] = this.coerceScalarValue(value);
406
+ this.setScalarValidity(element, value);
407
+ },
408
+ coerceScalarValue(rawValue) {
409
+ const value = rawValue === undefined || rawValue === null ? '' : String(rawValue).trim();
410
+ if (value === '__clear__') return '';
411
+ if (value === '') return '';
412
+ if (!${isNumericType}) return value;
413
+
414
+ const numeric = Number(value);
415
+ if (!Number.isFinite(numeric)) return value;
416
+
417
+ if (${isIntegerType} && !Number.isInteger(numeric)) {
418
+ return value;
419
+ }
420
+
421
+ return numeric;
422
+ },
423
+ setScalarValidity(element, rawValue) {
424
+ if (!element || typeof element.setCustomValidity !== 'function') return;
425
+ const value = rawValue === undefined || rawValue === null ? '' : String(rawValue).trim();
426
+ if (value === '' || value === '__clear__') {
427
+ element.setCustomValidity('');
428
+ return;
429
+ }
430
+
431
+ if (${isNumericType}) {
432
+ const numeric = Number(value);
433
+ if (!Number.isFinite(numeric)) {
434
+ element.setCustomValidity('Enter a valid number.');
435
+ return;
436
+ }
437
+ if (${isIntegerType} && !Number.isInteger(numeric)) {
438
+ element.setCustomValidity('Enter a whole number.');
439
+ return;
440
+ }
441
+ if (${hasNumericMinimum ? "true" : "false"} && numeric < ${numericMinimumLiteral}) {
442
+ element.setCustomValidity('Must be greater than or equal to ${numericMinimum ?? ""}.');
443
+ return;
444
+ }
445
+ if (${hasNumericExclusiveMinimum ? "true" : "false"} && numeric <= ${numericExclusiveMinimumLiteral}) {
446
+ element.setCustomValidity('Must be greater than ${numericExclusiveMinimum ?? ""}.');
447
+ return;
448
+ }
449
+ if (${hasNumericMaximum ? "true" : "false"} && numeric > ${numericMaximumLiteral}) {
450
+ element.setCustomValidity('Must be less than or equal to ${numericMaximum ?? ""}.');
451
+ return;
452
+ }
453
+ if (${hasNumericExclusiveMaximum ? "true" : "false"} && numeric >= ${numericExclusiveMaximumLiteral}) {
454
+ element.setCustomValidity('Must be less than ${numericExclusiveMaximum ?? ""}.');
455
+ return;
456
+ }
457
+ } else if (${hasStringType || hasStringMinLength || hasStringMaxLength ? "true" : "false"}) {
458
+ if (${hasStringMinLength ? "true" : "false"} && value.length < ${stringMinLengthLiteral}) {
459
+ element.setCustomValidity('Must be at least ${stringMinLength ?? ""} characters.');
460
+ return;
461
+ }
462
+ if (${hasStringMaxLength ? "true" : "false"} && value.length > ${stringMaxLengthLiteral}) {
463
+ element.setCustomValidity('Must be at most ${stringMaxLength ?? ""} characters.');
464
+ return;
465
+ }
466
+ }
467
+
468
+ element.setCustomValidity('');
469
+ },
470
+ reportFieldValidity(element) {
471
+ if (!element || typeof element.reportValidity !== 'function') return true;
472
+ this.setScalarValidity(element, element.value);
473
+ return element.reportValidity();
474
+ },
475
+ toBooleanOption(value) {
476
+ if (value === true || value === 'true') return 'true';
477
+ if (value === false || value === 'false') return 'false';
478
+ return '';
479
+ },
480
+ getBooleanFieldValue() {
481
+ return this.toBooleanOption(${fieldModelExpr});
482
+ },
483
+ setBooleanFieldValue(rawValue) {
484
+ const value = String(rawValue || '');
485
+ this.ensureScopeObject();
486
+ if (value === 'true') {
487
+ ${fieldModelExpr} = true;
488
+ return;
489
+ }
490
+ if (value === 'false') {
491
+ ${fieldModelExpr} = false;
492
+ return;
493
+ }
494
+ delete ${resolvedScopeExpr}[${fieldNameKey}];
495
+ },
496
+ getArrayBooleanValue(index) {
497
+ const values = this.getArrayValues();
498
+ return this.toBooleanOption(values[index]);
499
+ },
500
+ setArrayBooleanValue(index, rawValue) {
501
+ const value = String(rawValue || '');
502
+ const values = this.ensureArrayField();
503
+ if (value === 'true') {
504
+ values[index] = true;
505
+ return;
506
+ }
507
+ if (value === 'false') {
508
+ values[index] = false;
509
+ return;
510
+ }
511
+ values[index] = '';
512
+ },
513
+ getMapBooleanValue(mapKey) {
514
+ return this.toBooleanOption(this.getMapValue(mapKey));
515
+ },
516
+ setMapBooleanValue(mapKey, rawValue) {
517
+ const value = String(rawValue || '');
518
+ const target = this.ensureScopeObject();
519
+ if (value === 'true') {
520
+ target[mapKey] = true;
521
+ return;
522
+ }
523
+ if (value === 'false') {
524
+ target[mapKey] = false;
525
+ return;
526
+ }
527
+ target[mapKey] = '';
528
+ },
529
+ getMapJsonValue(mapKey) {
530
+ const target = this.ensureScopeObject();
531
+ const value = target[mapKey];
532
+ if (value === undefined) return '';
533
+ try {
534
+ return JSON.stringify(value, null, 2);
535
+ } catch (e) {
536
+ return '';
537
+ }
538
+ },
539
+ setMapJsonValue(mapKey, jsonValue) {
540
+ const target = this.ensureScopeObject();
541
+ const raw = String(jsonValue || '');
542
+ if (!raw.trim()) {
543
+ target[mapKey] = '';
544
+ return;
545
+ }
546
+ try {
547
+ target[mapKey] = JSON.parse(raw);
548
+ } catch (e) {
549
+ // Keep previous valid value while editing invalid JSON.
550
+ }
551
+ },
552
+ getFieldValue() {
553
+ const value = ${fieldModelExpr};
554
+ return value === undefined || value === null ? '' : value;
555
+ },
556
+ setFieldValue(value, element) {
557
+ this.ensureScopeObject();
558
+ ${fieldModelExpr} = this.coerceScalarValue(value);
559
+ this.setScalarValidity(element, value);
560
+ },
561
+ ensureArrayField() {
562
+ this.ensureScopeObject();
563
+ if (!Array.isArray(${fieldModelExpr})) {
564
+ ${fieldModelExpr} = [];
565
+ }
566
+ return ${fieldModelExpr};
567
+ },
568
+ getArrayValues() {
569
+ const values = ${fieldModelExpr};
570
+ return Array.isArray(values) && values.length > 0 ? values : [''];
571
+ },
572
+ setArrayValue(index, value, element) {
573
+ const values = this.ensureArrayField();
574
+ values[index] = this.coerceScalarValue(value);
575
+ this.setScalarValidity(element, value);
576
+ },
577
+ addArrayValue() {
578
+ const values = this.ensureArrayField();
579
+ if (values.length === 0) {
580
+ values.push('', '');
581
+ return;
582
+ }
583
+ values.push('');
584
+ },
585
+ removeArrayValue(index) {
586
+ const values = ${fieldModelExpr};
587
+ if (!Array.isArray(values)) return;
588
+ values.splice(index, 1);
589
+ if (values.length === 0) {
590
+ delete ${resolvedScopeExpr}[${fieldNameKey}];
591
+ }
592
+ },
593
+ getArrayObjectValues() {
594
+ const values = ${fieldModelExpr};
595
+ return Array.isArray(values) ? values : [];
596
+ },
597
+ ensureArrayObjectItem(index) {
598
+ const values = this.ensureArrayField();
599
+ if (
600
+ !values[index] ||
601
+ typeof values[index] !== 'object' ||
602
+ Array.isArray(values[index])
603
+ ) {
604
+ values[index] = {};
605
+ }
606
+ return values[index];
607
+ },
608
+ addArrayObjectValue() {
609
+ const values = this.ensureArrayField();
610
+ values.push({});
611
+ },
612
+ removeArrayObjectValue(index) {
613
+ const values = ${fieldModelExpr};
614
+ if (!Array.isArray(values)) return;
615
+ values.splice(index, 1);
616
+ this.shiftArrayItemVariantSelection(index);
617
+ if (values.length === 0) {
618
+ delete ${resolvedScopeExpr}[${fieldNameKey}];
619
+ }
620
+ }
621
+ }`}
622
+ x-effect="applyInitialDefault()"
623
+ class={layoutClass}
16
624
  >
17
625
  <div class="w-full mt-1.5">
18
626
  <Field
@@ -20,34 +628,472 @@ const { field, requestPart } = Astro.props;
20
628
  type={field.type}
21
629
  description={field.description}
22
630
  required={field.required}
631
+ minLength={field.minLength}
632
+ maxLength={field.maxLength}
633
+ minimum={field.minimum}
634
+ maximum={field.maximum}
635
+ exclusiveMinimum={field.exclusiveMinimum}
636
+ exclusiveMaximum={field.exclusiveMaximum}
23
637
  />
24
638
  </div>
25
639
  <div class="w-full">
26
640
  {
27
- field.enum && field.enum.length > 0 ? (
28
- <div class="relative">
29
- <select
30
- name={`${requestPart}_${field.name}`}
31
- x-model={`inputs['${requestPart}']['${field.name}']`}
32
- 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 placeholder:text-neutral-400 transition-all duration-200 bg-white appearance-none"
641
+ isAdditionalPropertyField ? (
642
+ <div class="space-y-2">
643
+ <template
644
+ x-for={`(mapKey, mapIndex) in getMapEntryKeys()`}
645
+ :key={`${fieldNameKey} + '-map-' + mapKey + '-' + mapIndex`}
33
646
  >
34
- {field.enum.map((value) => (
35
- <option value={String(value)}>{value}</option>
647
+ <div class="rounded-lg border border-neutral-200 bg-neutral-50/50 p-2.5">
648
+ <div class="mb-2 flex items-center gap-2">
649
+ <input
650
+ type="text"
651
+ :value="mapKey"
652
+ @change="renameMapKey(mapKey, $event.target.value)"
653
+ 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 placeholder:text-neutral-400 transition-all duration-200 bg-white"
654
+ placeholder="Property key"
655
+ />
656
+ <button
657
+ type="button"
658
+ @click="removeMapEntry(mapKey)"
659
+ class="h-[31px] px-2 rounded-md border border-neutral-200 text-neutral-500 hover:text-neutral-700 hover:border-neutral-300 hover:bg-white transition-colors duration-200 cursor-pointer"
660
+ aria-label={`Remove ${field.name} key`}
661
+ >
662
+ <Icon class="size-4" name="lucide:x" />
663
+ </button>
664
+ </div>
665
+ {
666
+ hasComplexMapValue ? (
667
+ <textarea
668
+ rows="3"
669
+ :value="getMapJsonValue(mapKey)"
670
+ @input="setMapJsonValue(mapKey, $event.target.value)"
671
+ 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"
672
+ placeholder="JSON value"
673
+ />
674
+ ) : field.enum && field.enum.length > 0 ? (
675
+ <div class="relative">
676
+ <select
677
+ :value="getMapValue(mapKey)"
678
+ @change="setMapValue(mapKey, $event.target.value, $event.target)"
679
+ :class="getMapValue(mapKey) ? 'text-neutral-700' : 'text-neutral-400'"
680
+ 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"
681
+ >
682
+ <option value="" disabled hidden>Select value</option>
683
+ <option value="__clear__">Clear selection</option>
684
+ {field.enum.map((value) => (
685
+ <option value={String(value)}>{value}</option>
686
+ ))}
687
+ </select>
688
+ <Icon
689
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
690
+ name="lucide:chevrons-up-down"
691
+ />
692
+ </div>
693
+ ) : isBooleanType ? (
694
+ <div class="relative">
695
+ <select
696
+ :value="getMapBooleanValue(mapKey)"
697
+ @change="setMapBooleanValue(mapKey, $event.target.value)"
698
+ :class="getMapBooleanValue(mapKey) ? 'text-neutral-700' : 'text-neutral-400'"
699
+ 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"
700
+ >
701
+ <option value="" disabled hidden>Select value</option>
702
+ <option value="__clear__">Clear selection</option>
703
+ <option value="true">true</option>
704
+ <option value="false">false</option>
705
+ </select>
706
+ <Icon
707
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
708
+ name="lucide:chevrons-up-down"
709
+ />
710
+ </div>
711
+ ) : (
712
+ <input
713
+ type={inputType}
714
+ :value="getMapValue(mapKey)"
715
+ @input="setMapValue(mapKey, $event.target.value, $event.target)"
716
+ @blur="reportFieldValidity($event.target)"
717
+ min={numericInputMin}
718
+ max={numericInputMax}
719
+ step={numericInputStep}
720
+ minlength={textInputMinLength}
721
+ maxlength={textInputMaxLength}
722
+ 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 placeholder:text-neutral-400 transition-all duration-200 bg-white"
723
+ placeholder="Property value"
724
+ />
725
+ )
726
+ }
727
+ </div>
728
+ </template>
729
+ <button
730
+ type="button"
731
+ @click="addMapEntry()"
732
+ class="inline-flex items-center gap-1 text-xs font-medium text-neutral-700 hover:text-neutral-900 cursor-pointer transition-colors duration-200"
733
+ >
734
+ <Icon class="size-3.5" name="lucide:plus" />
735
+ Add property
736
+ </button>
737
+ </div>
738
+ ) : field.isArray ? (
739
+ hasNestedFields || hasFieldVariants ? (
740
+ <div class="space-y-2">
741
+ <template
742
+ x-for={`(item, index) in getArrayObjectValues()`}
743
+ :key={`${fieldNameKey} + '-item-' + index`}
744
+ >
745
+ <div
746
+ class="rounded-lg border border-neutral-200 bg-neutral-50/50 p-2.5"
747
+ x-init="ensureArrayObjectItem(index)"
748
+ >
749
+ <div class="mb-2 flex items-center justify-between">
750
+ <span class="text-xs font-medium text-neutral-600">
751
+ Item <span x-text="index + 1" />
752
+ </span>
753
+ <button
754
+ type="button"
755
+ @click="removeArrayObjectValue(index)"
756
+ class="inline-flex h-6 items-center rounded-md border border-neutral-200 px-2 text-[11px] font-medium text-neutral-600 hover:border-neutral-300 hover:bg-white hover:text-neutral-700 transition-colors duration-200 cursor-pointer"
757
+ aria-label={`Remove ${field.name} item`}
758
+ >
759
+ Remove
760
+ </button>
761
+ </div>
762
+ <div class="space-y-3">
763
+ {field.nested?.map((nestedField) => (
764
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
765
+ <Astro.self
766
+ field={nestedField}
767
+ requestPart={requestPart}
768
+ scopeExpr={`${fieldModelExpr}[index]`}
769
+ defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
770
+ />
771
+ </div>
772
+ ))}
773
+ {field.variants && field.variants.length > 0 && (
774
+ <div class:list={["space-y-2", hasNestedFields && "pt-1"]}>
775
+ {hasOneOfVariants ? (
776
+ <>
777
+ <p class="text-xs text-neutral-500">
778
+ Select one variant.
779
+ </p>
780
+ <div class="inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg bg-neutral-100 p-1">
781
+ {field.variants.map((variant, variantIndex) => (
782
+ <button
783
+ type="button"
784
+ @click={`selectArrayItemVariant(index, ${variantIndex})`}
785
+ :class={`{
786
+ 'bg-white text-neutral-900 shadow-xs ring-1 ring-neutral-200': getSelectedArrayItemVariant(index) === ${variantIndex},
787
+ 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-800': getSelectedArrayItemVariant(index) !== ${variantIndex}
788
+ }`}
789
+ 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"
790
+ >
791
+ {variant.label}
792
+ </button>
793
+ ))}
794
+ </div>
795
+ {field.variants.map((variant, variantIndex) => (
796
+ <div
797
+ x-show={`getSelectedArrayItemVariant(index) === ${variantIndex}`}
798
+ x-cloak
799
+ >
800
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
801
+ <div class="mb-2 text-xs font-medium text-neutral-600">
802
+ {variant.label}
803
+ </div>
804
+ <div class="space-y-3">
805
+ {variant.fields.map((variantField) => (
806
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
807
+ <Astro.self
808
+ field={variantField}
809
+ requestPart={requestPart}
810
+ scopeExpr={`${fieldModelExpr}[index]`}
811
+ defaultsEnabledExpr={`(${resolvedDefaultsEnabledExpr}) && getSelectedArrayItemVariant(index) === ${variantIndex}`}
812
+ />
813
+ </div>
814
+ ))}
815
+ </div>
816
+ </div>
817
+ </div>
818
+ ))}
819
+ </>
820
+ ) : (
821
+ <>
822
+ <p class="text-xs text-neutral-500">
823
+ One or more variants may apply.
824
+ </p>
825
+ {field.variants.map((variant) => (
826
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
827
+ <div class="mb-2 text-xs font-medium text-neutral-600">
828
+ {variant.label}
829
+ </div>
830
+ <div class="space-y-3">
831
+ {variant.fields.map((variantField) => (
832
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
833
+ <Astro.self
834
+ field={variantField}
835
+ requestPart={requestPart}
836
+ scopeExpr={`${fieldModelExpr}[index]`}
837
+ defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
838
+ />
839
+ </div>
840
+ ))}
841
+ </div>
842
+ </div>
843
+ ))}
844
+ </>
845
+ )}
846
+ </div>
847
+ )}
848
+ </div>
849
+ </div>
850
+ </template>
851
+ <button
852
+ type="button"
853
+ @click="addArrayObjectValue()"
854
+ class="inline-flex items-center gap-1 text-xs font-medium text-neutral-700 hover:text-neutral-900 cursor-pointer transition-colors duration-200"
855
+ >
856
+ <Icon class="size-3.5" name="lucide:plus" />
857
+ Add item
858
+ </button>
859
+ </div>
860
+ ) : (
861
+ <div class="space-y-2">
862
+ <template x-for={`(value, index) in getArrayValues()`} :key={`${fieldNameKey} + '-' + index`}>
863
+ <div class="flex items-center gap-2">
864
+ {
865
+ field.enum && field.enum.length > 0 ? (
866
+ <div class="relative w-full">
867
+ <select
868
+ name={`${requestPart}_${field.name}`}
869
+ :value="value"
870
+ @change={`setArrayValue(index, $event.target.value, $event.target)`}
871
+ :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
872
+ :class="value ? 'text-neutral-700' : 'text-neutral-400'"
873
+ 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"
874
+ >
875
+ <option value="" disabled hidden>Select value</option>
876
+ <option value="__clear__">Clear selection</option>
877
+ {field.enum.map((value) => (
878
+ <option value={String(value)}>{value}</option>
879
+ ))}
880
+ </select>
881
+ <Icon
882
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
883
+ name="lucide:chevrons-up-down"
884
+ />
885
+ </div>
886
+ ) : (
887
+ isBooleanType ? (
888
+ <div class="relative w-full">
889
+ <select
890
+ name={`${requestPart}_${field.name}`}
891
+ :value="getArrayBooleanValue(index)"
892
+ @change={`setArrayBooleanValue(index, $event.target.value)`}
893
+ :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
894
+ :class="getArrayBooleanValue(index) ? 'text-neutral-700' : 'text-neutral-400'"
895
+ 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"
896
+ >
897
+ <option value="" disabled hidden>Select value</option>
898
+ <option value="__clear__">Clear selection</option>
899
+ <option value="true">true</option>
900
+ <option value="false">false</option>
901
+ </select>
902
+ <Icon
903
+ class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
904
+ name="lucide:chevrons-up-down"
905
+ />
906
+ </div>
907
+ ) : (
908
+ <input
909
+ name={field.name}
910
+ placeholder={`Enter ${field.name}`}
911
+ type={inputType}
912
+ :value="value"
913
+ @input={`setArrayValue(index, $event.target.value, $event.target)`}
914
+ @blur="reportFieldValidity($event.target)"
915
+ :required={`index === 0 && ${isRequiredField ? "true" : "false"}`}
916
+ min={numericInputMin}
917
+ max={numericInputMax}
918
+ step={numericInputStep}
919
+ minlength={textInputMinLength}
920
+ maxlength={textInputMaxLength}
921
+ 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 placeholder:text-neutral-400 transition-all duration-200"
922
+ />
923
+ )
924
+ )
925
+ }
926
+ <button
927
+ type="button"
928
+ @click="removeArrayValue(index)"
929
+ 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"
930
+ aria-label={`Remove ${field.name} value`}
931
+ >
932
+ <Icon class="size-4" name="lucide:x" />
933
+ </button>
934
+ </div>
935
+ </template>
936
+ <button
937
+ type="button"
938
+ @click="addArrayValue()"
939
+ class="inline-flex items-center gap-1 text-xs font-medium text-neutral-700 hover:text-neutral-900 cursor-pointer transition-colors duration-200"
940
+ >
941
+ <Icon class="size-3.5" name="lucide:plus" />
942
+ Add value
943
+ </button>
944
+ </div>
945
+ )
946
+ ) : hasNestedFields || hasFieldVariants ? (
947
+ <div class="rounded-lg border border-neutral-200 bg-neutral-50/50 p-2.5">
948
+ <div class="space-y-3">
949
+ {field.nested?.map((nestedField) => (
950
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
951
+ <Astro.self
952
+ field={nestedField}
953
+ requestPart={requestPart}
954
+ scopeExpr={fieldModelExpr}
955
+ defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
956
+ />
957
+ </div>
36
958
  ))}
37
- </select>
38
- <Icon
39
- class="absolute right-3 top-1/2 -translate-y-1/2 size-4 pointer-events-none text-neutral-400"
40
- name="lucide:chevrons-up-down"
41
- />
959
+ {field.variants && field.variants.length > 0 && (
960
+ <div class:list={["space-y-2", hasNestedFields && "pt-1"]}>
961
+ {hasOneOfVariants ? (
962
+ <>
963
+ <p class="text-xs text-neutral-500">
964
+ Select one variant.
965
+ </p>
966
+ <div class="inline-flex max-w-full flex-wrap items-center gap-1 rounded-lg bg-neutral-100 p-1">
967
+ {field.variants.map((variant, variantIndex) => (
968
+ <button
969
+ type="button"
970
+ @click={`selectOneOfVariant(${variantIndex})`}
971
+ :class={`{
972
+ 'bg-white text-neutral-900 shadow-xs ring-1 ring-neutral-200': getSelectedOneOfVariant() === ${variantIndex},
973
+ 'text-neutral-600 hover:bg-neutral-50 hover:text-neutral-800': getSelectedOneOfVariant() !== ${variantIndex}
974
+ }`}
975
+ 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"
976
+ >
977
+ {variant.label}
978
+ </button>
979
+ ))}
980
+ </div>
981
+ {field.variants.map((variant, variantIndex) => (
982
+ <div
983
+ x-show={`getSelectedOneOfVariant() === ${variantIndex}`}
984
+ x-cloak
985
+ >
986
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
987
+ <div class="mb-2 text-xs font-medium text-neutral-600">
988
+ {variant.label}
989
+ </div>
990
+ <div class="space-y-3">
991
+ {variant.fields.map((variantField) => (
992
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
993
+ <Astro.self
994
+ field={variantField}
995
+ requestPart={requestPart}
996
+ scopeExpr={fieldModelExpr}
997
+ defaultsEnabledExpr={`(${resolvedDefaultsEnabledExpr}) && getSelectedOneOfVariant() === ${variantIndex}`}
998
+ />
999
+ </div>
1000
+ ))}
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ ))}
1005
+ </>
1006
+ ) : (
1007
+ <>
1008
+ <p class="text-xs text-neutral-500">
1009
+ One or more variants may apply.
1010
+ </p>
1011
+ {field.variants.map((variant) => (
1012
+ <div class="rounded-md border border-neutral-200 bg-white p-2.5">
1013
+ <div class="mb-2 text-xs font-medium text-neutral-600">
1014
+ {variant.label}
1015
+ </div>
1016
+ <div class="space-y-3">
1017
+ {variant.fields.map((variantField) => (
1018
+ <div class="border-b border-b-neutral-100 last:border-none pb-3 last:pb-0">
1019
+ <Astro.self
1020
+ field={variantField}
1021
+ requestPart={requestPart}
1022
+ scopeExpr={fieldModelExpr}
1023
+ defaultsEnabledExpr={resolvedDefaultsEnabledExpr}
1024
+ />
1025
+ </div>
1026
+ ))}
1027
+ </div>
1028
+ </div>
1029
+ ))}
1030
+ </>
1031
+ )}
1032
+ </div>
1033
+ )}
1034
+ </div>
42
1035
  </div>
43
1036
  ) : (
44
- <input
45
- name={field.name}
46
- placeholder={`Enter ${field.name}`}
47
- type={field.type === "number" ? "number" : "text"}
48
- x-model={`inputs['${requestPart}']['${field.name}']`}
49
- 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 placeholder:text-neutral-400 transition-all duration-200"
50
- />
1037
+ <>
1038
+ {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' : 'text-neutral-400'"
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"
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
+ />
1058
+ </div>
1059
+ ) : 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' : 'text-neutral-400'"
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"
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
+ />
1078
+ </div>
1079
+ ) : (
1080
+ <input
1081
+ name={field.name}
1082
+ placeholder={`Enter ${field.name}`}
1083
+ type={inputType}
1084
+ :value="getFieldValue()"
1085
+ @input="setFieldValue($event.target.value, $event.target)"
1086
+ @blur="reportFieldValidity($event.target)"
1087
+ required={isRequiredField}
1088
+ min={numericInputMin}
1089
+ max={numericInputMax}
1090
+ step={numericInputStep}
1091
+ minlength={textInputMinLength}
1092
+ maxlength={textInputMaxLength}
1093
+ 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 placeholder:text-neutral-400 transition-all duration-200"
1094
+ />
1095
+ )}
1096
+ </>
51
1097
  )
52
1098
  }
53
1099
  </div>