schema-components 1.16.3 → 1.18.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 (48) hide show
  1. package/dist/core/adapter.d.mts +1 -1
  2. package/dist/core/constraints.d.mts +1 -1
  3. package/dist/core/diagnostics.d.mts +1 -1
  4. package/dist/core/merge.d.mts +14 -8
  5. package/dist/core/merge.mjs +81 -12
  6. package/dist/core/normalise.d.mts +1 -1
  7. package/dist/core/ref.d.mts +1 -1
  8. package/dist/core/renderer.d.mts +2 -2
  9. package/dist/core/renderer.mjs +50 -1
  10. package/dist/core/swagger2.d.mts +1 -1
  11. package/dist/core/typeInference.d.mts +2 -2
  12. package/dist/core/walkBuilders.d.mts +2 -2
  13. package/dist/core/walker.mjs +2 -2
  14. package/dist/{diagnostics-DzbZmcLI.d.mts → diagnostics-BYk63jsC.d.mts} +1 -1
  15. package/dist/html/a11y.d.mts +13 -2
  16. package/dist/html/a11y.mjs +26 -2
  17. package/dist/html/renderToHtml.d.mts +1 -1
  18. package/dist/html/renderToHtml.mjs +5 -3
  19. package/dist/html/renderToHtmlStream.d.mts +1 -1
  20. package/dist/html/renderers.d.mts +4 -3
  21. package/dist/html/renderers.mjs +9 -13
  22. package/dist/html/streamRenderers.d.mts +1 -1
  23. package/dist/openapi/bundle.d.mts +9 -4
  24. package/dist/openapi/bundle.mjs +73 -15
  25. package/dist/openapi/components.d.mts +1 -1
  26. package/dist/openapi/components.mjs +61 -27
  27. package/dist/openapi/parser.mjs +8 -8
  28. package/dist/openapi/resolve.d.mts +13 -2
  29. package/dist/openapi/resolve.mjs +19 -3
  30. package/dist/react/SchemaComponent.d.mts +35 -7
  31. package/dist/react/SchemaComponent.mjs +49 -43
  32. package/dist/react/SchemaView.d.mts +12 -4
  33. package/dist/react/SchemaView.mjs +24 -49
  34. package/dist/react/headless.d.mts +1 -1
  35. package/dist/react/headlessRenderers.d.mts +15 -2
  36. package/dist/react/headlessRenderers.mjs +52 -37
  37. package/dist/{ref-DvWoULcy.d.mts → ref-Ckt5liZs.d.mts} +1 -1
  38. package/dist/{renderer-BdSqllx5.d.mts → renderer-DXo-rXHJ.d.mts} +28 -2
  39. package/dist/themes/mantine.d.mts +6 -1
  40. package/dist/themes/mantine.mjs +44 -11
  41. package/dist/themes/mui.d.mts +1 -1
  42. package/dist/themes/mui.mjs +23 -8
  43. package/dist/themes/radix.d.mts +1 -1
  44. package/dist/themes/radix.mjs +43 -11
  45. package/dist/themes/shadcn.d.mts +1 -1
  46. package/dist/themes/shadcn.mjs +28 -10
  47. package/dist/{typeInference-k7FXfTVO.d.mts → typeInference-5JiqIZ8t.d.mts} +57 -4
  48. package/package.json +5 -2
@@ -2,13 +2,13 @@
2
2
  import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
4
4
  import { normaliseSchema } from "../core/adapter.mjs";
5
- import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
5
+ import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
6
6
  import { walk } from "../core/walker.mjs";
7
7
  import { headlessResolver } from "./headless.mjs";
8
8
  import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
9
9
  import { z } from "zod";
10
+ import { createContext, isValidElement, useCallback, useContext, useId, useMemo } from "react";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
- import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
12
12
  //#region src/react/SchemaComponent.tsx
13
13
  /**
14
14
  * <SchemaComponent> — renders UI from Zod, JSON Schema, or OpenAPI schemas.
@@ -46,9 +46,11 @@ const globalWidgets = /* @__PURE__ */ new Map();
46
46
  function registerWidget(name, render) {
47
47
  globalWidgets.set(name, render);
48
48
  }
49
- function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, onDiagnostic, strict, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets }) {
49
+ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, onDiagnostic, strict, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets, idPrefix }) {
50
50
  const userResolver = useContext(UserResolverContext);
51
51
  const contextWidgets = useContext(WidgetsContext);
52
+ const generatedId = useId();
53
+ const rootPath = idPrefix ?? sanitisePrefix(generatedId);
52
54
  const mergedMeta = useMemo(() => {
53
55
  const merged = { ...componentMeta };
54
56
  if (readOnly === true) merged.readOnly = true;
@@ -108,11 +110,40 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
108
110
  ...diagnostics !== void 0 ? { diagnostics } : {}
109
111
  };
110
112
  const tree = walk(jsonSchema, walkOptions);
111
- const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
112
- return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1), instanceWidgets, contextWidgets, currentDepth + 1);
113
+ const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
114
+ const childPath = joinPath(parentPath, pathSuffix);
115
+ return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, instanceWidgets, contextWidgets, currentDepth + 1);
113
116
  };
114
- const renderChild = makeRenderChild(0);
115
- return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets, 0);
117
+ const renderChild = makeRenderChild(0, rootPath);
118
+ return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, rootPath, instanceWidgets, contextWidgets, 0);
119
+ }
120
+ /**
121
+ * Default root-path sentinel used when no `idPrefix` is supplied AND the
122
+ * component is rendered outside a React tree (e.g. server-side bundling
123
+ * test harnesses). Production callers receive a `useId()`-derived prefix
124
+ * that is unique per instance.
125
+ */
126
+ const ROOT_PATH = "root";
127
+ /**
128
+ * Append a child path suffix to a parent path. When the suffix is omitted
129
+ * (e.g. transparent wrappers like union options), the parent path is
130
+ * returned unchanged so the child inherits the parent's id.
131
+ */
132
+ function joinPath(parent, suffix) {
133
+ if (suffix === void 0 || suffix.length === 0) return parent;
134
+ if (parent.length === 0) return suffix;
135
+ return `${parent}.${suffix}`;
136
+ }
137
+ /**
138
+ * Normalise a `useId()` value into a DOM-id-safe prefix. React's `useId`
139
+ * returns values containing `:` characters (e.g. `«:r0:»`) which are
140
+ * invalid in CSS selectors. Replace any run of non-alphanumeric characters
141
+ * with a single hyphen and trim leading/trailing hyphens.
142
+ */
143
+ function sanitisePrefix(value) {
144
+ const sanitised = value.replace(/[^a-zA-Z0-9_]+/g, "-").replace(/^-+|-+$/g, "");
145
+ if (sanitised.length === 0) throw new Error(`Cannot derive a DOM-safe id prefix from "${value}". Pass an explicit idPrefix prop.`);
146
+ return sanitised;
116
147
  }
117
148
  function runValidation(zodSchema, jsonSchema, value) {
118
149
  if (zodSchema !== void 0 && isObject(zodSchema)) {
@@ -134,7 +165,8 @@ function runValidation(zodSchema, jsonSchema, value) {
134
165
  }
135
166
  /** Maximum rendering depth before treating a field as recursive. */
136
167
  const MAX_RENDER_DEPTH = 10;
137
- function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets, depth = 0) {
168
+ function renderField(tree, value, onChange, userResolver, renderChild, path, instanceWidgets, contextWidgets, depth = 0) {
169
+ if (path.length === 0) throw new Error("renderField requires a non-empty path. Pass ROOT_PATH for the root field and use renderChild's pathSuffix to derive child paths.");
138
170
  if (depth >= MAX_RENDER_DEPTH) {
139
171
  const refTarget = tree.type === "recursive" ? tree.refTarget : "";
140
172
  return /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsxs("em", { children: [
@@ -147,7 +179,7 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
147
179
  if (typeof componentHint === "string") {
148
180
  const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
149
181
  if (widget !== void 0) {
150
- const result = widget(buildRenderProps(tree, value, onChange, renderChild));
182
+ const result = widget(buildRenderProps(tree, value, onChange, renderChild, path));
151
183
  if (result !== void 0 && result !== null) {
152
184
  if (isValidElement(result)) return result;
153
185
  if (typeof result === "string" || typeof result === "number") return result;
@@ -160,7 +192,7 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
160
192
  if (renderFn !== void 0) {
161
193
  let result;
162
194
  try {
163
- result = renderFn(buildRenderProps(tree, value, onChange, renderChild));
195
+ result = renderFn(buildRenderProps(tree, value, onChange, renderChild, path));
164
196
  } catch (err) {
165
197
  throw new SchemaRenderError(err instanceof Error ? err.message : `Render function threw for type "${tree.type}"`, tree, tree.type, err);
166
198
  }
@@ -171,38 +203,10 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
171
203
  if (value === void 0 || value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
172
204
  return /* @__PURE__ */ jsx("span", { children: typeof value === "string" ? value : JSON.stringify(value) });
173
205
  }
174
- function buildRenderProps(tree, value, onChange, renderChild) {
175
- const props = {
176
- value,
177
- onChange,
178
- readOnly: tree.editability === "presentation",
179
- writeOnly: tree.editability === "input",
180
- meta: tree.meta,
181
- constraints: tree.constraints,
182
- path: "",
183
- tree,
184
- renderChild
185
- };
186
- if (tree.type === "enum") props.enumValues = tree.enumValues;
187
- if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
188
- if (tree.type === "object") props.fields = tree.fields;
189
- if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
190
- if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
191
- if (tree.type === "record") props.keyType = tree.keyType;
192
- if (tree.type === "record") props.valueType = tree.valueType;
193
- if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
194
- if (tree.type === "conditional") props.ifClause = tree.ifClause;
195
- if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
196
- if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
197
- if (tree.type === "negation") props.negated = tree.negated;
198
- if (tree.type === "recursive") props.refTarget = tree.refTarget;
199
- if (tree.type === "literal") props.literalValues = tree.literalValues;
200
- if (tree.examples !== void 0) props.examples = tree.examples;
201
- return props;
202
- }
203
206
  function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
204
207
  const userResolver = useContext(UserResolverContext);
205
208
  const contextWidgets = useContext(WidgetsContext);
209
+ const generatedId = useId();
206
210
  let jsonSchema;
207
211
  let zodSchema;
208
212
  let rootMeta;
@@ -240,10 +244,12 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
240
244
  onChange,
241
245
  onValidationError
242
246
  ]);
243
- const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
244
- return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1), void 0, contextWidgets, currentDepth + 1);
247
+ const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
248
+ const childPath = joinPath(parentPath, pathSuffix);
249
+ return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, void 0, contextWidgets, currentDepth + 1);
245
250
  };
246
- return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0), void 0, contextWidgets, 0);
251
+ const rootPath = joinPath(sanitisePrefix(generatedId), path);
252
+ return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0, rootPath), rootPath, void 0, contextWidgets, 0);
247
253
  }
248
254
  /**
249
255
  * Dispatch Zod errors to per-field onValidationError callbacks.
@@ -290,4 +296,4 @@ function detectNormalisationKind(err) {
290
296
  return "unknown";
291
297
  }
292
298
  //#endregion
293
- export { SchemaComponent, SchemaField, SchemaProvider, registerWidget, renderField };
299
+ export { ROOT_PATH, SchemaComponent, SchemaField, SchemaProvider, joinPath, registerWidget, renderField, sanitisePrefix };
@@ -1,6 +1,6 @@
1
1
  import { T as SchemaMeta } from "../types-D_5ST7SS.mjs";
2
- import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
3
- import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
2
+ import { t as Diagnostic } from "../diagnostics-BYk63jsC.mjs";
3
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
4
4
  import { WidgetMap } from "./SchemaComponent.mjs";
5
5
  import { ReactNode } from "react";
6
6
 
@@ -30,9 +30,16 @@ interface SchemaViewProps {
30
30
  onDiagnostic?: (diagnostic: Diagnostic) => void;
31
31
  /** When true, any diagnostic becomes a thrown error. */
32
32
  strict?: boolean;
33
+ /**
34
+ * Prefix used for every input `id`/label `htmlFor` in this view subtree.
35
+ * Defaults to a per-instance value from `useId()`; pass a deterministic
36
+ * value when stable ids matter (e.g. snapshot tests).
37
+ */
38
+ idPrefix?: string;
33
39
  }
34
40
  /**
35
- * Server-safe schema renderer — no hooks, no context, no state.
41
+ * Server-safe schema renderer — no context and no state. The only hook
42
+ * called is `useId()`, which is RSC-safe.
36
43
  *
37
44
  * Always renders in read-only mode. For editable forms, use
38
45
  * `<SchemaComponent>` with `"use client"`.
@@ -47,7 +54,8 @@ declare function SchemaView({
47
54
  resolver,
48
55
  widgets,
49
56
  onDiagnostic,
50
- strict
57
+ strict,
58
+ idPrefix
51
59
  }: SchemaViewProps): ReactNode;
52
60
  //#endregion
53
61
  export { SchemaView, SchemaViewProps };
@@ -1,17 +1,21 @@
1
1
  import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
2
2
  import { normaliseSchema } from "../core/adapter.mjs";
3
- import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
3
+ import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
4
4
  import { walk } from "../core/walker.mjs";
5
5
  import { headlessResolver } from "./headless.mjs";
6
+ import { joinPath, sanitisePrefix } from "./SchemaComponent.mjs";
7
+ import { createElement, isValidElement, useId } from "react";
6
8
  import { jsx } from "react/jsx-runtime";
7
- import { createElement, isValidElement } from "react";
8
9
  //#region src/react/SchemaView.tsx
9
10
  /**
10
11
  * React Server Component for read-only schema rendering.
11
12
  *
12
- * This component has zero hooks — no `useContext`, no `useMemo`,
13
- * no `useCallback`. It can run in a React Server Component environment
14
- * without the `"use client"` directive.
13
+ * Uses no React state, context, or effects — no `useContext`, `useMemo`,
14
+ * `useCallback`, `useState`, or `useEffect`. The single hook called is
15
+ * `useId()`, which is one of the few hooks permitted inside a React
16
+ * Server Component (it is RSC-safe by design) and is used solely to
17
+ * derive a stable per-instance `idPrefix`. The component therefore runs
18
+ * in an RSC environment without the `"use client"` directive.
15
19
  *
16
20
  * **Read-only only.** For interactive forms with `onChange`, use
17
21
  * `<SchemaComponent>` (which requires `"use client"`).
@@ -30,14 +34,16 @@ import { createElement, isValidElement } from "react";
30
34
  * Server Components cannot use React context, so the resolver
31
35
  * is passed explicitly.
32
36
  */
33
- function noop() {}
34
37
  /**
35
- * Server-safe schema renderer — no hooks, no context, no state.
38
+ * Server-safe schema renderer — no context and no state. The only hook
39
+ * called is `useId()`, which is RSC-safe.
36
40
  *
37
41
  * Always renders in read-only mode. For editable forms, use
38
42
  * `<SchemaComponent>` with `"use client"`.
39
43
  */
40
- function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict }) {
44
+ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict, idPrefix }) {
45
+ const generatedId = useId();
46
+ const rootPath = idPrefix ?? sanitisePrefix(generatedId);
41
47
  const mergedMeta = {
42
48
  ...componentMeta,
43
49
  readOnly: true
@@ -68,29 +74,22 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
68
74
  const tree = walk(jsonSchema, walkOptions);
69
75
  const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
70
76
  const MAX_SERVER_DEPTH = 10;
71
- const makeRenderChild = (currentDepth) => (childTree, childValue) => {
77
+ const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, pathSuffix) => {
78
+ const childPath = joinPath(parentPath, pathSuffix);
72
79
  if (currentDepth >= MAX_SERVER_DEPTH) return createElement("fieldset", null, createElement("em", null, `\u21bb ${typeof childTree.meta.description === "string" ? childTree.meta.description : childTree.type === "recursive" ? childTree.refTarget : "schema"} (recursive)`));
73
- return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1), widgets);
80
+ return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, widgets);
74
81
  };
75
- const renderChild = makeRenderChild(0);
76
- return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
82
+ const renderChild = makeRenderChild(0, rootPath);
83
+ return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, rootPath, widgets);
77
84
  }
78
- function renderFieldServer(tree, value, resolver, renderChild, widgets) {
85
+ function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
86
+ if (path.length === 0) throw new Error("renderFieldServer requires a non-empty path. Pass ROOT_PATH at the root and join children via joinPath().");
87
+ const adaptedRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
79
88
  const componentHint = tree.meta.component;
80
89
  if (typeof componentHint === "string") {
81
90
  const widget = widgets?.get(componentHint);
82
91
  if (widget !== void 0) {
83
- const result = widget({
84
- value,
85
- onChange: noop,
86
- readOnly: true,
87
- writeOnly: false,
88
- meta: tree.meta,
89
- constraints: tree.constraints,
90
- path: "",
91
- tree,
92
- renderChild: (childTree, childValue) => renderChild(childTree, childValue)
93
- });
92
+ const result = widget(buildRenderProps(tree, value, void 0, adaptedRenderChild, path));
94
93
  if (result !== void 0 && result !== null) {
95
94
  if (isValidElement(result)) return result;
96
95
  if (typeof result === "string" || typeof result === "number") return result;
@@ -99,31 +98,7 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
99
98
  }
100
99
  const renderFn = getRenderFunction(tree.type, resolver);
101
100
  if (renderFn !== void 0) {
102
- const props = {
103
- value,
104
- onChange: noop,
105
- readOnly: true,
106
- writeOnly: false,
107
- meta: tree.meta,
108
- constraints: tree.constraints,
109
- path: "",
110
- tree,
111
- renderChild: (childTree, childValue) => renderChild(childTree, childValue)
112
- };
113
- if (tree.type === "enum") props.enumValues = tree.enumValues;
114
- if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
115
- if (tree.type === "object") props.fields = tree.fields;
116
- if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
117
- if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
118
- if (tree.type === "record") props.keyType = tree.keyType;
119
- if (tree.type === "record") props.valueType = tree.valueType;
120
- if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
121
- if (tree.type === "conditional") props.ifClause = tree.ifClause;
122
- if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
123
- if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
124
- if (tree.type === "negation") props.negated = tree.negated;
125
- if (tree.type === "recursive") props.refTarget = tree.refTarget;
126
- if (tree.type === "literal") props.literalValues = tree.literalValues;
101
+ const props = buildRenderProps(tree, value, void 0, adaptedRenderChild, path);
127
102
  try {
128
103
  const result = renderFn(props);
129
104
  if (result !== void 0 && result !== null) {
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
1
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
2
2
 
3
3
  //#region src/react/headless.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { l as RenderProps } from "../renderer-BdSqllx5.mjs";
2
+ import { l as RenderProps } from "../renderer-DXo-rXHJ.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/react/headlessRenderers.d.ts
@@ -8,6 +8,19 @@ import { ReactNode } from "react";
8
8
  * Returns `null` for unrecognised values.
9
9
  */
10
10
  declare function toReactNode(value: unknown): ReactNode;
11
+ /**
12
+ * Build a stable, unique input ID from the path.
13
+ * Used for `htmlFor`/`id` association between labels and inputs.
14
+ *
15
+ * Throws on an empty path: the previous "sc-field" fallback caused every
16
+ * input across a form to share the same id, breaking label-input pairing
17
+ * and screen reader navigation. Callers must thread a non-empty path
18
+ * (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
19
+ *
20
+ * Dots and bracket indices in paths are converted to hyphens to keep the
21
+ * id valid as a CSS selector and predictable in test queries.
22
+ */
23
+ declare function inputId(path: string): string;
11
24
  declare function renderString(props: RenderProps): ReactNode;
12
25
  declare function renderNumber(props: RenderProps): ReactNode;
13
26
  declare function renderBoolean(props: RenderProps): ReactNode;
@@ -46,4 +59,4 @@ declare function renderFile(props: RenderProps): ReactNode;
46
59
  declare function renderRecursive(props: RenderProps): ReactNode;
47
60
  declare function renderUnknown(props: RenderProps): ReactNode;
48
61
  //#endregion
49
- export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
62
+ export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
@@ -1,6 +1,6 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
+ import { isValidElement, useCallback, useEffect, useRef } from "react";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { isValidElement, useCallback, useRef } from "react";
4
4
  //#region src/react/headlessRenderers.tsx
5
5
  /**
6
6
  * Headless renderer functions — one per schema type.
@@ -57,12 +57,20 @@ function dateInputType(format) {
57
57
  if (format === "date-time" || format === "datetime") return "datetime-local";
58
58
  }
59
59
  /**
60
- * Build a stable, unique-ish input ID from the path.
60
+ * Build a stable, unique input ID from the path.
61
61
  * Used for `htmlFor`/`id` association between labels and inputs.
62
+ *
63
+ * Throws on an empty path: the previous "sc-field" fallback caused every
64
+ * input across a form to share the same id, breaking label-input pairing
65
+ * and screen reader navigation. Callers must thread a non-empty path
66
+ * (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
67
+ *
68
+ * Dots and bracket indices in paths are converted to hyphens to keep the
69
+ * id valid as a CSS selector and predictable in test queries.
62
70
  */
63
71
  function inputId(path) {
64
- if (path.length === 0) return "sc-field";
65
- return `sc-${path}`;
72
+ 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.");
73
+ return `sc-${path.replace(/[.[\]]+/g, "-").replace(/-+$/g, "")}`;
66
74
  }
67
75
  function renderString(props) {
68
76
  const id = inputId(props.path);
@@ -245,14 +253,14 @@ function renderObject(props) {
245
253
  });
246
254
  return /* @__PURE__ */ jsxs("fieldset", { children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx("legend", { children: props.meta.description }), sortedEntries.filter(([, field]) => field.meta.visible !== false).map(([key, field]) => {
247
255
  const childValue = obj[key];
248
- const childId = inputId(props.path ? `${props.path}.${key}` : key);
256
+ const childId = inputId(`${props.path}.${key}`);
249
257
  const childOnChange = (v) => {
250
258
  const updated = {};
251
259
  for (const [k, val] of Object.entries(obj)) updated[k] = val;
252
260
  updated[key] = v;
253
261
  props.onChange(updated);
254
262
  };
255
- const child = toReactNode(props.renderChild(field, childValue, childOnChange));
263
+ const child = toReactNode(props.renderChild(field, childValue, childOnChange, key));
256
264
  if (child === null || child === void 0) return null;
257
265
  return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsxs("label", {
258
266
  htmlFor: childId,
@@ -318,9 +326,9 @@ function renderRecord(props) {
318
326
  "aria-label": props.meta.description ?? "Record",
319
327
  children: entries.map(([key, value]) => {
320
328
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
321
- htmlFor: inputId(props.path ? `${props.path}.${key}` : key),
329
+ htmlFor: inputId(`${props.path}.${key}`),
322
330
  children: key
323
- }), toReactNode(props.renderChild(valueType, value, () => {}))] }, key);
331
+ }), toReactNode(props.renderChild(valueType, value, () => {}, key))] }, key);
324
332
  })
325
333
  });
326
334
  }
@@ -355,7 +363,7 @@ function renderRecord(props) {
355
363
  children: [entries.map(([key, value]) => {
356
364
  return /* @__PURE__ */ jsxs("div", { children: [
357
365
  /* @__PURE__ */ jsx("input", {
358
- id: `${inputId(props.path ? `${props.path}.${key}` : key)}-key`,
366
+ id: `${inputId(`${props.path}.${key}`)}-key`,
359
367
  type: "text",
360
368
  "aria-label": "Entry key",
361
369
  defaultValue: key,
@@ -365,7 +373,7 @@ function renderRecord(props) {
365
373
  }),
366
374
  toReactNode(props.renderChild(valueType, value, (nextValue) => {
367
375
  handleValueChange(key, nextValue);
368
- })),
376
+ }, key)),
369
377
  /* @__PURE__ */ jsx("button", {
370
378
  type: "button",
371
379
  "aria-label": `Remove entry ${key}`,
@@ -397,7 +405,7 @@ function renderArray(props) {
397
405
  next[i] = v;
398
406
  props.onChange(next);
399
407
  };
400
- return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
408
+ return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange, `[${String(i)}]`)) }, String(i));
401
409
  })
402
410
  });
403
411
  }
@@ -467,14 +475,20 @@ function discriminatedUnionValueForTab(optionLabels, discKey, newIndex) {
467
475
  }
468
476
  /**
469
477
  * WAI-ARIA tabs component for discriminated unions.
470
- * Implements the full tabs keyboard pattern:
471
- * - Left/Right arrow keys move between tabs
472
- * - Home/End move to first/last tab
473
- * - Tab moves focus into the active panel
474
- * - aria-selected, aria-controls, role="tablist"/"tab"/"tabpanel"
478
+ *
479
+ * Implements the WAI-ARIA "Tabs with Automatic Activation" pattern
480
+ * (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/):
481
+ * - ArrowRight / ArrowLeft move between tabs, wrapping at the extremes
482
+ * - Home / End jump to the first / last tab
483
+ * - aria-selected, aria-controls, role="tablist" / "tab" / "tabpanel"
484
+ * - Roving tabindex: the active tab has tabindex=0, the rest tabindex=-1
485
+ *
486
+ * "Automatic activation" means each arrow key both moves focus and
487
+ * activates the new tab in one step — selection and focus stay aligned.
475
488
  */
476
489
  function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
477
490
  const tabRefs = useRef([]);
491
+ const pendingFocusRef = useRef(false);
478
492
  const handleTabChange = useCallback((newIndex) => {
479
493
  const next = discriminatedUnionValueForTab(optionLabels, discKey, newIndex);
480
494
  if (next === void 0) return;
@@ -482,31 +496,31 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
482
496
  }, [
483
497
  optionLabels,
484
498
  discKey,
485
- props
499
+ props.onChange
486
500
  ]);
487
- const focusTab = useCallback((index) => {
488
- const clamped = (index % options.length + options.length) % options.length;
489
- tabRefs.current[clamped]?.focus();
490
- }, [options.length]);
501
+ const wrapIndex = useCallback((index) => (index % options.length + options.length) % options.length, [options.length]);
491
502
  const handleKeyDown = useCallback((e) => {
492
- if (e.key === "ArrowRight") {
493
- e.preventDefault();
494
- focusTab(activeIndex + 1);
495
- } else if (e.key === "ArrowLeft") {
496
- e.preventDefault();
497
- focusTab(activeIndex - 1);
498
- } else if (e.key === "Home") {
499
- e.preventDefault();
500
- focusTab(0);
501
- } else if (e.key === "End") {
502
- e.preventDefault();
503
- focusTab(options.length - 1);
504
- }
503
+ let target;
504
+ if (e.key === "ArrowRight") target = wrapIndex(activeIndex + 1);
505
+ else if (e.key === "ArrowLeft") target = wrapIndex(activeIndex - 1);
506
+ else if (e.key === "Home") target = 0;
507
+ else if (e.key === "End") target = options.length - 1;
508
+ if (target === void 0) return;
509
+ e.preventDefault();
510
+ if (target === activeIndex) return;
511
+ pendingFocusRef.current = true;
512
+ handleTabChange(target);
505
513
  }, [
506
514
  activeIndex,
507
- focusTab,
508
- options.length
515
+ handleTabChange,
516
+ options.length,
517
+ wrapIndex
509
518
  ]);
519
+ useEffect(() => {
520
+ if (!pendingFocusRef.current) return;
521
+ pendingFocusRef.current = false;
522
+ tabRefs.current[activeIndex]?.focus();
523
+ }, [activeIndex]);
510
524
  const activeOption = options[activeIndex];
511
525
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
512
526
  role: "tablist",
@@ -523,6 +537,7 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
523
537
  },
524
538
  type: "button",
525
539
  role: "tab",
540
+ id: `${panelId}-tab-${String(i)}`,
526
541
  "aria-selected": i === activeIndex ? "true" : void 0,
527
542
  "aria-controls": `${panelId}-panel`,
528
543
  tabIndex: i === activeIndex ? 0 : -1,
@@ -608,4 +623,4 @@ function matchUnionOption(options, value) {
608
623
  if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
609
624
  }
610
625
  //#endregion
611
- export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
626
+ export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "./diagnostics-DzbZmcLI.mjs";
1
+ import { i as DiagnosticsOptions } from "./diagnostics-BYk63jsC.mjs";
2
2
 
3
3
  //#region src/core/ref.d.ts
4
4
  /**
@@ -69,8 +69,17 @@ interface RenderProps extends BaseFieldProps {
69
69
  * Render a child field. Theme adapters call this to recursively render
70
70
  * nested structures (object fields, array elements, union options).
71
71
  * The resolver and rendering context are already wired in.
72
+ *
73
+ * @param tree - The walked field tree for the child
74
+ * @param value - The child's current value
75
+ * @param onChange - Callback receiving the child's next value
76
+ * @param pathSuffix - Path segment from the parent (e.g. "city",
77
+ * "[0]"). Joined to the parent's path with a dot, or substituted
78
+ * when the parent acts as a transparent wrapper (union options).
79
+ * Required for every container — without it children inherit no
80
+ * path and `inputId()` will throw.
72
81
  */
73
- renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => unknown;
82
+ renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void, pathSuffix?: string) => unknown;
74
83
  }
75
84
  /**
76
85
  * Props for HTML render functions. Extends BaseFieldProps with:
@@ -90,6 +99,23 @@ interface HtmlRenderProps extends BaseFieldProps {
90
99
  */
91
100
  renderChild: (tree: WalkedField, value: unknown, pathSuffix?: string) => string;
92
101
  }
102
+ /**
103
+ * Build the `RenderProps` object handed to a resolver render function or a
104
+ * widget. Used by both the server-side `<SchemaView>` (which has no
105
+ * `onChange`) and the client-side `<SchemaComponent>` (which threads an
106
+ * `onChange` callback).
107
+ *
108
+ * When `onChange` is `undefined` the caller is rendering in read-only mode:
109
+ * a noop `onChange` is wired up, `readOnly` is forced to `true`, and
110
+ * `writeOnly` is forced to `false`. Otherwise the editability is taken
111
+ * from `tree.editability`.
112
+ *
113
+ * The duplicate sibling fields (`enumValues`, `element`, `fields`, etc.)
114
+ * are populated for backwards compatibility with renderers that have not
115
+ * yet migrated to reading from `tree` directly. New renderers should
116
+ * narrow on `tree.type` and read from `tree`.
117
+ */
118
+ declare function buildRenderProps(tree: WalkedField, value: unknown, onChange: ((next: unknown) => void) | undefined, renderChild: RenderProps["renderChild"], path: string): RenderProps;
93
119
  type RenderFunction = (props: RenderProps) => unknown;
94
120
  interface ComponentResolver {
95
121
  string?: RenderFunction;
@@ -157,4 +183,4 @@ declare function mergeResolvers(user: ComponentResolver, fallback: ComponentReso
157
183
  */
158
184
  declare function mergeHtmlResolvers(user: HtmlResolver, fallback: HtmlResolver): HtmlResolver;
159
185
  //#endregion
160
- export { HtmlRenderProps as a, RenderFunction as c, getRenderFunction as d, mergeHtmlResolvers as f, HtmlRenderFunction as i, RenderProps as l, typeToKey as m, BaseFieldProps as n, HtmlResolver as o, mergeResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, getHtmlRenderFn as u };
186
+ export { HtmlRenderProps as a, RenderFunction as c, getHtmlRenderFn as d, getRenderFunction as f, typeToKey as h, HtmlRenderFunction as i, RenderProps as l, mergeResolvers as m, BaseFieldProps as n, HtmlResolver as o, mergeHtmlResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, buildRenderProps as u };
@@ -1,9 +1,13 @@
1
- import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
1
+ import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
2
2
 
3
3
  //#region src/themes/mantine.d.ts
4
4
  /**
5
5
  * Register real Mantine components for the resolver to use.
6
6
  * Call once at app startup before rendering.
7
+ *
8
+ * `Text` is required so read-only scalars render as a styled Mantine
9
+ * `<Text>` element instead of a bare `<span>`, matching the visual
10
+ * weight of the editable variants.
7
11
  */
8
12
  declare function registerMantineComponents(components: {
9
13
  TextInput: React.ElementType;
@@ -11,6 +15,7 @@ declare function registerMantineComponents(components: {
11
15
  Switch: React.ElementType;
12
16
  Select: React.ElementType;
13
17
  Fieldset: React.ElementType;
18
+ Text: React.ElementType;
14
19
  }): void;
15
20
  declare const mantineResolver: ComponentResolver;
16
21
  //#endregion