schema-components 1.21.0 → 1.23.0

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 (91) hide show
  1. package/README.md +3 -1
  2. package/dist/core/adapter.d.mts +115 -4
  3. package/dist/core/adapter.mjs +405 -75
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -7
  6. package/dist/core/cssClasses.d.mts +52 -0
  7. package/dist/core/cssClasses.mjs +51 -0
  8. package/dist/core/diagnostics.d.mts +1 -1
  9. package/dist/core/errors.d.mts +1 -1
  10. package/dist/core/errors.mjs +5 -13
  11. package/dist/core/fieldOrder.d.mts +1 -1
  12. package/dist/core/formats.d.mts +30 -2
  13. package/dist/core/formats.mjs +33 -1
  14. package/dist/core/idPath.d.mts +54 -0
  15. package/dist/core/idPath.mjs +66 -0
  16. package/dist/core/limits.d.mts +2 -0
  17. package/dist/core/limits.mjs +23 -0
  18. package/dist/core/merge.d.mts +10 -1
  19. package/dist/core/merge.mjs +49 -10
  20. package/dist/core/normalise.d.mts +40 -3
  21. package/dist/core/normalise.mjs +2 -2
  22. package/dist/core/openapi30.d.mts +15 -1
  23. package/dist/core/openapi30.mjs +2 -2
  24. package/dist/core/openapiConstants.d.mts +67 -0
  25. package/dist/core/openapiConstants.mjs +90 -0
  26. package/dist/core/ref.d.mts +2 -2
  27. package/dist/core/ref.mjs +85 -6
  28. package/dist/core/refChain.d.mts +70 -0
  29. package/dist/core/refChain.mjs +44 -0
  30. package/dist/core/renderer.d.mts +1 -1
  31. package/dist/core/renderer.mjs +0 -2
  32. package/dist/core/swagger2.d.mts +1 -1
  33. package/dist/core/swagger2.mjs +1 -1
  34. package/dist/core/typeInference.d.mts +982 -2
  35. package/dist/core/types.d.mts +2 -2
  36. package/dist/core/types.mjs +1 -4
  37. package/dist/core/unionMatch.d.mts +36 -0
  38. package/dist/core/unionMatch.mjs +53 -0
  39. package/dist/core/version.d.mts +1 -1
  40. package/dist/core/version.mjs +29 -17
  41. package/dist/core/walkBuilders.d.mts +23 -4
  42. package/dist/core/walkBuilders.mjs +27 -7
  43. package/dist/core/walker.d.mts +1 -1
  44. package/dist/core/walker.mjs +123 -47
  45. package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-BS2kaUyE.d.mts} +1 -1
  46. package/dist/{errors-QEwOtQAA.d.mts → errors-g_MCTQel.d.mts} +10 -16
  47. package/dist/html/a11y.d.mts +9 -4
  48. package/dist/html/a11y.mjs +10 -12
  49. package/dist/html/renderToHtml.d.mts +10 -3
  50. package/dist/html/renderToHtml.mjs +13 -3
  51. package/dist/html/renderToHtmlStream.d.mts +2 -2
  52. package/dist/html/renderToHtmlStream.mjs +12 -1
  53. package/dist/html/renderers.d.mts +43 -8
  54. package/dist/html/renderers.mjs +136 -116
  55. package/dist/html/streamRenderers.d.mts +6 -6
  56. package/dist/html/streamRenderers.mjs +129 -89
  57. package/dist/limits-Cw5QZND8.d.mts +29 -0
  58. package/dist/{normalise-DaSrnr8g.mjs → normalise-DCYp06Sr.mjs} +770 -227
  59. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  60. package/dist/openapi/ApiLinks.d.mts +1 -1
  61. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  62. package/dist/openapi/ApiSecurity.d.mts +1 -1
  63. package/dist/openapi/ApiSecurity.mjs +16 -2
  64. package/dist/openapi/components.d.mts +234 -23
  65. package/dist/openapi/components.mjs +183 -52
  66. package/dist/openapi/parser.d.mts +9 -8
  67. package/dist/openapi/parser.mjs +252 -70
  68. package/dist/openapi/resolve.d.mts +31 -15
  69. package/dist/openapi/resolve.mjs +260 -40
  70. package/dist/react/SchemaComponent.d.mts +126 -36
  71. package/dist/react/SchemaComponent.mjs +95 -57
  72. package/dist/react/SchemaView.d.mts +30 -10
  73. package/dist/react/SchemaView.mjs +2 -2
  74. package/dist/react/a11y.d.mts +21 -0
  75. package/dist/react/a11y.mjs +24 -0
  76. package/dist/react/fieldPath.d.mts +1 -1
  77. package/dist/react/headless.d.mts +1 -1
  78. package/dist/react/headless.mjs +1 -2
  79. package/dist/react/headlessRenderers.d.mts +9 -11
  80. package/dist/react/headlessRenderers.mjs +51 -102
  81. package/dist/{ref-si8ViYun.d.mts → ref-DjLEKa_E.d.mts} +38 -3
  82. package/dist/{renderer-DI6ZYf7a.d.mts → renderer-CXJ8y0qw.d.mts} +2 -2
  83. package/dist/themes/mantine.d.mts +1 -1
  84. package/dist/themes/mui.d.mts +1 -1
  85. package/dist/themes/radix.d.mts +1 -1
  86. package/dist/themes/shadcn.d.mts +1 -1
  87. package/dist/themes/shadcn.mjs +2 -1
  88. package/dist/{types-BnxPEElk.d.mts → types-BTB73MB8.d.mts} +35 -14
  89. package/dist/{version-D-u7aMfy.d.mts → version-BFTVLsdb.d.mts} +7 -1
  90. package/package.json +1 -3
  91. package/dist/typeInference-Bxw3NOG1.d.mts +0 -647
@@ -1,6 +1,12 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
+ import { dateInputType } from "../core/formats.mjs";
2
3
  import { isSafeHyperlink, isSafeMailtoAddress } from "../core/uri.mjs";
4
+ import { SC_CLASSES } from "../core/cssClasses.mjs";
3
5
  import { sortFieldsByOrder } from "../core/fieldOrder.mjs";
6
+ import { fieldDomId, panelIdFor, tabIdFor } from "../core/idPath.mjs";
7
+ import { matchUnionOption, resolveDiscriminatedActive } from "../core/unionMatch.mjs";
8
+ import { displayJsonValue } from "../core/walkBuilders.mjs";
9
+ import { buildAriaAttrs } from "./a11y.mjs";
4
10
  import { jsx, jsxs } from "react/jsx-runtime";
5
11
  import { isValidElement, useCallback, useEffect, useRef } from "react";
6
12
  //#region src/react/headlessRenderers.tsx
@@ -25,54 +31,35 @@ function toReactNode(value) {
25
31
  }
26
32
  function formatDateTime(value) {
27
33
  if (typeof value !== "string" || value.length === 0) return void 0;
28
- try {
29
- const date = new Date(value);
30
- if (isNaN(date.getTime())) return void 0;
31
- return date.toLocaleString();
32
- } catch {
33
- return;
34
- }
34
+ const date = new Date(value);
35
+ if (isNaN(date.getTime())) return void 0;
36
+ return date.toLocaleString();
35
37
  }
36
38
  function formatDate(value) {
37
39
  if (typeof value !== "string" || value.length === 0) return void 0;
38
- try {
39
- const date = new Date(value);
40
- if (isNaN(date.getTime())) return void 0;
41
- return date.toLocaleDateString();
42
- } catch {
43
- return;
44
- }
40
+ const date = new Date(value);
41
+ if (isNaN(date.getTime())) return void 0;
42
+ return date.toLocaleDateString();
45
43
  }
46
44
  function formatTime(value) {
47
45
  if (typeof value !== "string" || value.length === 0) return void 0;
48
- try {
49
- const date = new Date(value);
50
- if (isNaN(date.getTime())) return void 0;
51
- return date.toLocaleTimeString();
52
- } catch {
53
- return;
54
- }
55
- }
56
- function dateInputType(format) {
57
- if (format === "date") return "date";
58
- if (format === "time") return "time";
59
- if (format === "date-time" || format === "datetime") return "datetime-local";
46
+ const date = new Date(value);
47
+ if (isNaN(date.getTime())) return void 0;
48
+ return date.toLocaleTimeString();
60
49
  }
61
50
  /**
62
51
  * Build a stable, unique input ID from the path.
63
- * Used for `htmlFor`/`id` association between labels and inputs.
64
52
  *
65
- * Throws on an empty path: the previous "sc-field" fallback caused every
66
- * input across a form to share the same id, breaking label-input pairing
67
- * and screen reader navigation. Callers must thread a non-empty path
68
- * (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
53
+ * Re-exported alias for {@link fieldDomId} so external themes (shadcn,
54
+ * MUI, mantine, radix) keep importing `inputId` from the React entry
55
+ * point. Both the React and HTML renderers must derive the same id from
56
+ * the same path `fieldDomId` in `core/idPath.ts` is the single
57
+ * source-of-truth.
69
58
  *
70
- * Dots and bracket indices in paths are converted to hyphens to keep the
71
- * id valid as a CSS selector and predictable in test queries.
59
+ * Throws on an empty path; see `fieldDomId` for the rationale.
72
60
  */
73
61
  function inputId(path) {
74
- if (path.length === 0) throw new Error("inputId requires a non-empty path. Pass ROOT_PATH for the root field and use renderChild's pathSuffix to derive child paths.");
75
- return `sc-${path.replace(/[.[\]]+/g, "-").replace(/-+$/g, "")}`;
62
+ return fieldDomId(path);
76
63
  }
77
64
  function renderString(props) {
78
65
  const id = inputId(props.path);
@@ -119,8 +106,7 @@ function renderString(props) {
119
106
  }
120
107
  const strValue = typeof props.value === "string" ? props.value : "";
121
108
  const dateType = dateInputType(props.constraints.format);
122
- const ariaAttrs = {};
123
- if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
109
+ const ariaAttrs = buildAriaAttrs(props.tree);
124
110
  if (dateType !== void 0) return /* @__PURE__ */ jsx("input", {
125
111
  id,
126
112
  type: dateType,
@@ -143,7 +129,7 @@ function renderString(props) {
143
129
  value: "",
144
130
  children: ["Select", "…"]
145
131
  }), enumValues.map((v) => {
146
- const display = v === null ? "null" : typeof v === "string" ? v : String(v);
132
+ const display = displayJsonValue(v);
147
133
  return /* @__PURE__ */ jsx("option", {
148
134
  value: display,
149
135
  children: display
@@ -179,8 +165,7 @@ function renderNumber(props) {
179
165
  });
180
166
  }
181
167
  const numValue = typeof props.value === "number" ? props.value : "";
182
- const ariaAttrs = {};
183
- if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
168
+ const ariaAttrs = buildAriaAttrs(props.tree);
184
169
  return /* @__PURE__ */ jsx("input", {
185
170
  id,
186
171
  type: "number",
@@ -207,9 +192,7 @@ function renderBoolean(props) {
207
192
  children: props.value ? "Yes" : "No"
208
193
  });
209
194
  }
210
- const ariaAttrs = {};
211
- if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
212
- if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
195
+ const ariaAttrs = buildAriaAttrs(props.tree, props.meta.description);
213
196
  return /* @__PURE__ */ jsx("input", {
214
197
  id,
215
198
  type: "checkbox",
@@ -228,8 +211,7 @@ function renderEnum(props) {
228
211
  "aria-readonly": "true",
229
212
  children: enumValue || "—"
230
213
  });
231
- const ariaAttrs = {};
232
- if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
214
+ const ariaAttrs = buildAriaAttrs(props.tree);
233
215
  const enumValues = props.tree.type === "enum" ? props.tree.enumValues : [];
234
216
  return /* @__PURE__ */ jsxs("select", {
235
217
  id,
@@ -242,7 +224,7 @@ function renderEnum(props) {
242
224
  value: "",
243
225
  children: ["Select", "…"]
244
226
  }), enumValues.map((v) => {
245
- const display = v === null ? "null" : typeof v === "string" ? v : String(v);
227
+ const display = displayJsonValue(v);
246
228
  return /* @__PURE__ */ jsx("option", {
247
229
  value: display,
248
230
  children: display
@@ -327,7 +309,7 @@ function renderRecord(props) {
327
309
  });
328
310
  return /* @__PURE__ */ jsx("div", {
329
311
  role: "group",
330
- "aria-label": props.meta.description ?? "Record",
312
+ "aria-label": typeof props.meta.description === "string" ? props.meta.description : void 0,
331
313
  children: entries.map(([key, value]) => {
332
314
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
333
315
  htmlFor: inputId(`${props.path}.${key}`),
@@ -427,32 +409,16 @@ function renderUnion(props) {
427
409
  return /* @__PURE__ */ jsx("span", { children: "—" });
428
410
  }
429
411
  function renderDiscriminatedUnion(props) {
430
- const options = props.tree.type === "discriminatedUnion" ? props.tree.options : void 0;
431
- const discriminator = props.tree.type === "discriminatedUnion" ? props.tree.discriminator : void 0;
432
- if (options === void 0 || options.length === 0) {
412
+ if (props.tree.type !== "discriminatedUnion") {
433
413
  if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
434
414
  return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
435
415
  }
436
- const obj = isObject(props.value) ? props.value : {};
437
- const discKey = discriminator ?? "";
438
- const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
439
- const optionLabels = options.map((opt) => {
440
- if (opt.type === "object") {
441
- const discriminatorField = opt.fields[discKey];
442
- if (discriminatorField?.type === "literal") {
443
- const constVal = discriminatorField.literalValues[0];
444
- if (typeof constVal === "string") return constVal;
445
- }
446
- }
447
- return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
448
- });
449
- let activeIndex = 0;
450
- if (currentDiscriminatorValue !== void 0) {
451
- const found = optionLabels.indexOf(currentDiscriminatorValue);
452
- if (found !== -1) activeIndex = found;
416
+ const { options, discriminator: discKey } = props.tree;
417
+ if (options.length === 0) {
418
+ if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
419
+ return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
453
420
  }
454
- const activeOption = options[activeIndex];
455
- const panelId = inputId(props.path);
421
+ const { optionLabels, activeIndex, activeOption } = resolveDiscriminatedActive(options, discKey, isObject(props.value) ? props.value : void 0);
456
422
  if (props.readOnly) {
457
423
  if (activeOption !== void 0) return toReactNode(props.renderChild(activeOption, props.value, props.onChange));
458
424
  return /* @__PURE__ */ jsx("span", { children: "—" });
@@ -461,7 +427,7 @@ function renderDiscriminatedUnion(props) {
461
427
  options,
462
428
  optionLabels,
463
429
  activeIndex,
464
- panelId,
430
+ path: props.path,
465
431
  discKey,
466
432
  props
467
433
  });
@@ -491,7 +457,8 @@ function discriminatedUnionValueForTab(optionLabels, discKey, newIndex) {
491
457
  * "Automatic activation" means each arrow key both moves focus and
492
458
  * activates the new tab in one step — selection and focus stay aligned.
493
459
  */
494
- function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
460
+ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, path, discKey, props }) {
461
+ const panelId = panelIdFor(path);
495
462
  const tabRefs = useRef([]);
496
463
  const pendingFocusRef = useRef(false);
497
464
  const handleTabChange = useCallback((newIndex) => {
@@ -542,9 +509,9 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
542
509
  },
543
510
  type: "button",
544
511
  role: "tab",
545
- id: `${panelId}-tab-${String(i)}`,
512
+ id: tabIdFor(path, i),
546
513
  "aria-selected": i === activeIndex ? "true" : void 0,
547
- "aria-controls": `${panelId}-panel`,
514
+ "aria-controls": panelId,
548
515
  tabIndex: i === activeIndex ? 0 : -1,
549
516
  onClick: () => {
550
517
  handleTabChange(i);
@@ -561,8 +528,8 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
561
528
  }, String(i)))
562
529
  }), /* @__PURE__ */ jsx("div", {
563
530
  role: "tabpanel",
564
- id: `${panelId}-panel`,
565
- "aria-labelledby": `${panelId}-tab-${String(activeIndex)}`,
531
+ id: panelId,
532
+ "aria-labelledby": tabIdFor(path, activeIndex),
566
533
  children: activeOption !== void 0 && toReactNode(props.renderChild(activeOption, props.value, props.onChange))
567
534
  })] });
568
535
  }
@@ -574,9 +541,6 @@ function renderFile(props) {
574
541
  "aria-readonly": "true",
575
542
  children: "File field"
576
543
  });
577
- const ariaAttrs = {};
578
- if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
579
- if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
580
544
  return /* @__PURE__ */ jsx("input", {
581
545
  id,
582
546
  type: "file",
@@ -585,17 +549,9 @@ function renderFile(props) {
585
549
  const file = e.target.files?.[0];
586
550
  if (file !== void 0) props.onChange(file);
587
551
  },
588
- ...ariaAttrs
552
+ ...buildAriaAttrs(props.tree, props.meta.description)
589
553
  });
590
554
  }
591
- function renderRecursive(props) {
592
- const refTarget = props.tree.type === "recursive" ? props.tree.refTarget : "";
593
- return /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsxs("em", { children: [
594
- "↻ ",
595
- typeof props.meta.description === "string" ? props.meta.description : refTarget,
596
- " (recursive)"
597
- ] }) });
598
- }
599
555
  /**
600
556
  * Render a literal field — `z.literal("a")` or `{ const: 5 }`.
601
557
  *
@@ -615,7 +571,7 @@ function renderLiteral(props) {
615
571
  return /* @__PURE__ */ jsx("span", {
616
572
  id,
617
573
  "aria-readonly": "true",
618
- children: values.map((v) => v === null ? "null" : String(v)).join(", ")
574
+ children: values.map((v) => displayJsonValue(v)).join(", ")
619
575
  });
620
576
  }
621
577
  /**
@@ -644,7 +600,7 @@ function renderNever(props) {
644
600
  return /* @__PURE__ */ jsx("span", {
645
601
  id: inputId(props.path),
646
602
  "aria-readonly": "true",
647
- className: "sc-never",
603
+ className: SC_CLASSES.never,
648
604
  children: /* @__PURE__ */ jsx("em", { children: "never matches" })
649
605
  });
650
606
  }
@@ -698,10 +654,10 @@ function renderConditional(props) {
698
654
  if (props.tree.type !== "conditional") return null;
699
655
  const { ifClause, thenClause, elseClause } = props.tree;
700
656
  return /* @__PURE__ */ jsxs("fieldset", {
701
- className: "sc-conditional",
657
+ className: SC_CLASSES.conditional,
702
658
  children: [
703
659
  /* @__PURE__ */ jsxs("div", {
704
- className: "sc-conditional-if",
660
+ className: SC_CLASSES.conditionalIf,
705
661
  children: [
706
662
  /* @__PURE__ */ jsx("strong", { children: "if:" }),
707
663
  " ",
@@ -709,7 +665,7 @@ function renderConditional(props) {
709
665
  ]
710
666
  }),
711
667
  thenClause !== void 0 && /* @__PURE__ */ jsxs("div", {
712
- className: "sc-conditional-then",
668
+ className: SC_CLASSES.conditionalThen,
713
669
  children: [
714
670
  /* @__PURE__ */ jsx("strong", { children: "then:" }),
715
671
  " ",
@@ -717,7 +673,7 @@ function renderConditional(props) {
717
673
  ]
718
674
  }),
719
675
  elseClause !== void 0 && /* @__PURE__ */ jsxs("div", {
720
- className: "sc-conditional-else",
676
+ className: SC_CLASSES.conditionalElse,
721
677
  children: [
722
678
  /* @__PURE__ */ jsx("strong", { children: "else:" }),
723
679
  " ",
@@ -737,7 +693,7 @@ function renderConditional(props) {
737
693
  function renderNegation(props) {
738
694
  if (props.tree.type !== "negation") return null;
739
695
  return /* @__PURE__ */ jsxs("fieldset", {
740
- className: "sc-negation",
696
+ className: SC_CLASSES.negation,
741
697
  children: [
742
698
  /* @__PURE__ */ jsx("strong", { children: "Must NOT match:" }),
743
699
  " ",
@@ -769,12 +725,5 @@ function renderUnknown(props) {
769
725
  }
770
726
  });
771
727
  }
772
- function matchUnionOption(options, value) {
773
- if (typeof value === "string") return options.find((o) => o.type === "string" || o.type === "enum");
774
- if (typeof value === "number") return options.find((o) => o.type === "number");
775
- if (typeof value === "boolean") return options.find((o) => o.type === "boolean");
776
- if (Array.isArray(value)) return options.find((o) => o.type === "array");
777
- if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
778
- }
779
728
  //#endregion
780
- export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderTuple, renderUnion, renderUnknown, toReactNode };
729
+ export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderString, renderTuple, renderUnion, renderUnknown, toReactNode };
@@ -1,6 +1,13 @@
1
- import { i as DiagnosticsOptions } from "./diagnostics-CbBPsxSt.mjs";
1
+ import { i as DiagnosticsOptions } from "./diagnostics-BS2kaUyE.mjs";
2
2
 
3
3
  //#region src/core/ref.d.ts
4
+ /**
5
+ * The canonical recursive `$anchor` name synthesised by the Draft
6
+ * 2019-09 `$recursiveAnchor: true` rewrite. Re-exported here so the
7
+ * collision check in {@link findAnchor} stays aligned with the
8
+ * rewriter in `core/normalise.ts`.
9
+ */
10
+ declare const RECURSIVE_ANCHOR_SENTINEL = "__recursive__";
4
11
  /**
5
12
  * Resolver function for external $ref URIs.
6
13
  * Called with the URI portion (everything before `#`) of an external ref.
@@ -33,17 +40,45 @@ declare function resolveRef(schema: Record<string, unknown>, rootDocument: Recor
33
40
  /**
34
41
  * Dereference a JSON Pointer fragment (`#/path/to/schema`) or an
35
42
  * `$anchor` (`#SomeName`) against a root document.
43
+ *
44
+ * Returns the resolved sub-schema, which may be a JSON object or — per
45
+ * Draft 06+ — a boolean (`true` for the always-valid schema, `false`
46
+ * for the never-valid schema). Returns `undefined` when the pointer or
47
+ * anchor cannot be resolved.
48
+ *
49
+ * JSON Pointer segments are percent-decoded per RFC 6901 §6 before the
50
+ * `~1`/`~0` token expansion; this allows pointers such as
51
+ * `#/paths/~1pets%20store` to resolve a path containing a literal space.
36
52
  */
37
- declare function dereference(ref: string, root: Record<string, unknown>): Record<string, unknown> | undefined;
53
+ declare function dereference(ref: string, root: Record<string, unknown>): Record<string, unknown> | boolean | undefined;
38
54
  /**
39
55
  * Recursively scan a schema document for a `$anchor` matching the given name.
40
56
  * Returns the schema object containing the anchor, or undefined.
41
57
  *
58
+ * Per JSON Schema 2020-12 §8.2, `$anchor` is scoped to the resource
59
+ * defined by the nearest enclosing `$id`. A bare DFS would happily
60
+ * cross resource boundaries and resolve to an anchor declared in an
61
+ * unrelated sub-resource — that violates the spec and produces wrong
62
+ * walker input when two sub-schemas use the same anchor name within
63
+ * their own `$id` scope.
64
+ *
65
+ * The walk skips into any sub-tree that introduces a new `$id` value:
66
+ * such a sub-tree is a separate resource and its `$anchor`s belong to
67
+ * that resource, not the caller's. Anchors declared at the same `$id`
68
+ * scope (or in nested sub-schemas without their own `$id`) remain
69
+ * reachable.
70
+ *
42
71
  * The optional `visited` set guards against shared object references and
43
72
  * cycles introduced by the OpenAPI bundler's `structuredClone`-based
44
73
  * inlining of external refs. Without it a recursive document would stack
45
74
  * overflow before reaching the matching anchor.
75
+ *
76
+ * When `crossResourceBoundary` is `true` the walker is currently
77
+ * recursing into a sub-tree that introduced its own `$id`; we still
78
+ * recurse so a nested `$anchor` declared inside that same sub-resource
79
+ * is reachable from the caller that owns that resource, but we skip
80
+ * further nested resources for the same reason as above.
46
81
  */
47
82
  declare function findAnchor(node: unknown, anchorName: string, visited?: WeakSet<object>): Record<string, unknown> | undefined;
48
83
  //#endregion
49
- export { findAnchor as a, dereference as i, RefOptions as n, resolveRef as o, countDistinctRefs as r, ExternalResolver as t };
84
+ export { dereference as a, countDistinctRefs as i, RECURSIVE_ANCHOR_SENTINEL as n, findAnchor as o, RefOptions as r, resolveRef as s, ExternalResolver as t };
@@ -1,4 +1,4 @@
1
- import { D as StringConstraints, M as WalkedField, T as SchemaMeta, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "./types-BnxPEElk.mjs";
1
+ import { E as StringConstraints, f as FileConstraints, j as WalkedField, t as ArrayConstraints, w as SchemaMeta, x as ObjectConstraints, y as NumberConstraints } from "./types-BTB73MB8.mjs";
2
2
 
3
3
  //#region src/core/renderer.d.ts
4
4
  /**
@@ -140,7 +140,7 @@ interface HtmlResolver {
140
140
  never?: HtmlRenderFunction;
141
141
  unknown?: HtmlRenderFunction;
142
142
  }
143
- declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "null", "enum", "object", "array", "tuple", "record", "union", "discriminatedUnion", "conditional", "negation", "recursive", "literal", "file", "never", "unknown"];
143
+ declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "null", "enum", "object", "array", "tuple", "record", "union", "discriminatedUnion", "conditional", "negation", "literal", "file", "never", "unknown"];
144
144
  type ResolverKey = (typeof RESOLVER_KEYS)[number];
145
145
  /**
146
146
  * Map a schema type to the resolver key that handles it.
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-DI6ZYf7a.mjs";
1
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
2
2
 
3
3
  //#region src/themes/mantine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-DI6ZYf7a.mjs";
1
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
2
2
 
3
3
  //#region src/themes/mui.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-DI6ZYf7a.mjs";
1
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
2
2
 
3
3
  //#region src/themes/radix.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-DI6ZYf7a.mjs";
1
+ import { r as ComponentResolver } from "../renderer-CXJ8y0qw.mjs";
2
2
 
3
3
  //#region src/themes/shadcn.d.ts
4
4
  declare const shadcnResolver: ComponentResolver;
@@ -1,5 +1,6 @@
1
1
  import { toRecord } from "../core/guards.mjs";
2
2
  import { sortFieldsByOrder } from "../core/fieldOrder.mjs";
3
+ import { displayJsonValue } from "../core/walkBuilders.mjs";
3
4
  import { inputId, toReactNode } from "../react/headlessRenderers.mjs";
4
5
  import { headlessResolver } from "../react/headless.mjs";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -160,7 +161,7 @@ function renderEnumInput(props) {
160
161
  value: "",
161
162
  children: ["Select", "…"]
162
163
  }), props.tree.type === "enum" ? props.tree.enumValues.map((v) => {
163
- const display = v === null ? "null" : typeof v === "string" ? v : String(v);
164
+ const display = displayJsonValue(v);
164
165
  return /* @__PURE__ */ jsx("option", {
165
166
  value: display,
166
167
  children: display
@@ -59,7 +59,7 @@ type FieldOverride = Partial<SchemaMeta> & {
59
59
  * All schema types the walker can produce.
60
60
  * Used as the discriminant in the WalkedField tagged union.
61
61
  */
62
- type SchemaType = "string" | "number" | "boolean" | "null" | "enum" | "literal" | "object" | "array" | "tuple" | "record" | "union" | "discriminatedUnion" | "conditional" | "negation" | "recursive" | "file" | "never" | "unknown";
62
+ type SchemaType = "string" | "number" | "boolean" | "null" | "enum" | "literal" | "object" | "array" | "tuple" | "record" | "union" | "discriminatedUnion" | "conditional" | "negation" | "file" | "never" | "unknown";
63
63
  /** Constraints that apply to string schemas. */
64
64
  interface StringConstraints {
65
65
  minLength?: number;
@@ -86,8 +86,6 @@ interface ArrayConstraints {
86
86
  uniqueItems?: boolean;
87
87
  minContains?: number;
88
88
  maxContains?: number;
89
- /** Constraint schema for unevaluated items. */
90
- unevaluatedItems?: Record<string, unknown>;
91
89
  }
92
90
  /** Constraints that apply to object schemas. */
93
91
  interface ObjectConstraints {
@@ -138,12 +136,24 @@ interface NullField extends FieldBase {
138
136
  interface EnumField extends FieldBase {
139
137
  type: "enum";
140
138
  constraints: Record<string, never>;
141
- enumValues: (string | number | boolean | null)[];
139
+ /**
140
+ * Enum values from JSON Schema `enum`. Per Draft 2020-12 §6.1.2,
141
+ * `enum` accepts an array of arbitrary JSON values — not only
142
+ * primitives. Object and array values are preserved verbatim.
143
+ */
144
+ enumValues: unknown[];
142
145
  }
143
146
  interface LiteralField extends FieldBase {
144
147
  type: "literal";
145
148
  constraints: Record<string, never>;
146
- literalValues: (string | number | boolean | null)[];
149
+ /**
150
+ * Const values from JSON Schema `const`. Per Draft 2020-12 §6.1.3,
151
+ * `const` accepts any JSON value (object or array included), not
152
+ * only primitives. The walker emits a single-element array because
153
+ * `const` is scalar by definition; the field shape mirrors
154
+ * `EnumField` for renderer symmetry.
155
+ */
156
+ literalValues: unknown[];
147
157
  }
148
158
  interface ObjectField extends FieldBase {
149
159
  type: "object";
@@ -182,6 +192,13 @@ interface ArrayField extends FieldBase {
182
192
  contains?: WalkedField;
183
193
  /** Walked schema for unevaluated items. */
184
194
  unevaluatedItems?: WalkedField;
195
+ /**
196
+ * Whether `unevaluatedItems` is explicitly `false` (no extras
197
+ * permitted beyond the items evaluated by `items`/`prefixItems`/
198
+ * `contains`). Parallel to `additionalPropertiesClosed` and
199
+ * `unevaluatedPropertiesClosed` on `ObjectField`.
200
+ */
201
+ unevaluatedItemsClosed?: boolean;
185
202
  }
186
203
  interface TupleField extends FieldBase {
187
204
  type: "tuple";
@@ -200,6 +217,17 @@ interface TupleField extends FieldBase {
200
217
  * element schemas to require the presence of a specific element.
201
218
  */
202
219
  contains?: WalkedField;
220
+ /**
221
+ * Walked schema for `unevaluatedItems` adjacent to `prefixItems`.
222
+ * Permits additional items only when they satisfy this schema.
223
+ */
224
+ unevaluatedItems?: WalkedField;
225
+ /**
226
+ * Whether `unevaluatedItems` is explicitly `false` on a tuple. With
227
+ * `prefixItems` declared, this forbids any items beyond the prefix
228
+ * length.
229
+ */
230
+ unevaluatedItemsClosed?: boolean;
203
231
  }
204
232
  interface RecordField extends FieldBase {
205
233
  type: "record";
@@ -243,12 +271,6 @@ interface FileField extends FieldBase {
243
271
  type: "file";
244
272
  constraints: FileConstraints;
245
273
  }
246
- interface RecursiveField extends FieldBase {
247
- type: "recursive";
248
- constraints: Record<string, never>;
249
- /** The $ref string that would create the cycle (e.g. "#" or "#Node"). */
250
- refTarget: string;
251
- }
252
274
  /** Schema position where `false` appears — the field cannot have any value. */
253
275
  interface NeverField extends FieldBase {
254
276
  type: "never";
@@ -262,7 +284,7 @@ interface UnknownField extends FieldBase {
262
284
  * Tagged union of all schema field types produced by the walker.
263
285
  * Use `field.type` to narrow to a specific variant.
264
286
  */
265
- type WalkedField = StringField | NumberField | BooleanField | NullField | EnumField | LiteralField | ObjectField | ArrayField | TupleField | RecordField | UnionField | DiscriminatedUnionField | ConditionalField | NegationField | RecursiveField | NeverField | FileField | UnknownField;
287
+ type WalkedField = StringField | NumberField | BooleanField | NullField | EnumField | LiteralField | ObjectField | ArrayField | TupleField | RecordField | UnionField | DiscriminatedUnionField | ConditionalField | NegationField | NeverField | FileField | UnknownField;
266
288
  declare function isStringField(field: WalkedField): field is StringField;
267
289
  declare function isNumberField(field: WalkedField): field is NumberField;
268
290
  declare function isBooleanField(field: WalkedField): field is BooleanField;
@@ -278,8 +300,7 @@ declare function isDiscriminatedUnionField(field: WalkedField): field is Discrim
278
300
  declare function isConditionalField(field: WalkedField): field is ConditionalField;
279
301
  declare function isNegationField(field: WalkedField): field is NegationField;
280
302
  declare function isFileField(field: WalkedField): field is FileField;
281
- declare function isRecursiveField(field: WalkedField): field is RecursiveField;
282
303
  declare function isNeverField(field: WalkedField): field is NeverField;
283
304
  declare function isUnknownField(field: WalkedField): field is UnknownField;
284
305
  //#endregion
285
- export { UnionField as A, isNegationField as B, RecordField as C, StringConstraints as D, SchemaType as E, isConditionalField as F, isRecordField as G, isNullField as H, isDiscriminatedUnionField as I, isTupleField as J, isRecursiveField as K, isEnumField as L, WalkedField as M, isArrayField as N, StringField as O, isBooleanField as P, isFileField as R, ObjectField as S, SchemaMeta as T, isNumberField as U, isNeverField as V, isObjectField as W, isUnknownField as X, isUnionField as Y, resolveEditability as Z, NeverField as _, DiscriminatedUnionField as a, NumberField as b, FieldBase as c, FieldOverrides as d, FileConstraints as f, NegationField as g, LiteralField as h, ConditionalField as i, UnknownField as j, TupleField as k, FieldConstraints as l, JsonObject as m, ArrayField as n, Editability as o, FileField as p, isStringField as q, BooleanField as r, EnumField as s, ArrayConstraints as t, FieldOverride as u, NullField as v, RecursiveField as w, ObjectConstraints as x, NumberConstraints as y, isLiteralField as z };
306
+ export { UnknownField as A, isNeverField as B, RecordField as C, StringField as D, StringConstraints as E, isDiscriminatedUnionField as F, isStringField as G, isNumberField as H, isEnumField as I, isUnknownField as J, isTupleField as K, isFileField as L, isArrayField as M, isBooleanField as N, TupleField as O, isConditionalField as P, isLiteralField as R, ObjectField as S, SchemaType as T, isObjectField as U, isNullField as V, isRecordField as W, resolveEditability as Y, NeverField as _, DiscriminatedUnionField as a, NumberField as b, FieldBase as c, FieldOverrides as d, FileConstraints as f, NegationField as g, LiteralField as h, ConditionalField as i, WalkedField as j, UnionField as k, FieldConstraints as l, JsonObject as m, ArrayField as n, Editability as o, FileField as p, isUnionField as q, BooleanField as r, EnumField as s, ArrayConstraints as t, FieldOverride as u, NullField as v, SchemaMeta as w, ObjectConstraints as x, NumberConstraints as y, isNegationField as z };
@@ -56,7 +56,13 @@ interface OpenApiVersionInfo {
56
56
  }
57
57
  /**
58
58
  * Detect the OpenAPI/Swagger version from a document.
59
- * Returns `undefined` for documents that are not OpenAPI or Swagger.
59
+ *
60
+ * Returns `undefined` when the document declares neither `swagger` nor
61
+ * `openapi` strings, or when the declared version string is malformed
62
+ * (missing parts or non-numeric segments). `Number("abc")` yields `NaN`,
63
+ * which `parts[i] ?? default` does NOT replace — nullish coalescing only
64
+ * substitutes `null`/`undefined` — so any unparseable segment surfaces
65
+ * as `undefined` rather than a silent fabricated default.
60
66
  */
61
67
  declare function detectOpenApiVersion(doc: Record<string, unknown>): OpenApiVersionInfo | undefined;
62
68
  /**
package/package.json CHANGED
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
- "main": "./dist/index.mjs",
7
- "types": "./dist/index.d.mts",
8
6
  "exports": {
9
7
  "./styles.css": "./dist/html/styles.css",
10
8
  "./core/*": {