schema-components 1.16.2 → 1.17.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.
@@ -1,2 +1,2 @@
1
- import { a as HtmlRenderProps, c as RenderFunction, d as getRenderFunction, f as mergeHtmlResolvers, i as HtmlRenderFunction, l as RenderProps, m as typeToKey, n as BaseFieldProps, o as HtmlResolver, p as mergeResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as getHtmlRenderFn } from "../renderer-BdSqllx5.mjs";
1
+ import { a as HtmlRenderProps, c as RenderFunction, d as getRenderFunction, f as mergeHtmlResolvers, i as HtmlRenderFunction, l as RenderProps, m as typeToKey, n as BaseFieldProps, o as HtmlResolver, p as mergeResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as getHtmlRenderFn } from "../renderer-B3s8o2B8.mjs";
2
2
  export { AllConstraints, BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { t as AllConstraints } from "../renderer-BdSqllx5.mjs";
2
+ import { t as AllConstraints } from "../renderer-B3s8o2B8.mjs";
3
3
  import { HtmlAttributes, HtmlNode } from "./html.mjs";
4
4
 
5
5
  //#region src/html/a11y.d.ts
@@ -1,5 +1,5 @@
1
1
  import { T as SchemaMeta } from "../types-D_5ST7SS.mjs";
2
- import { o as HtmlResolver } from "../renderer-BdSqllx5.mjs";
2
+ import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
3
3
 
4
4
  //#region src/html/renderToHtml.d.ts
5
5
  interface RenderToHtmlOptions {
@@ -1,5 +1,5 @@
1
1
  import { T as SchemaMeta } from "../types-D_5ST7SS.mjs";
2
- import { o as HtmlResolver } from "../renderer-BdSqllx5.mjs";
2
+ import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
3
3
 
4
4
  //#region src/html/renderToHtmlStream.d.ts
5
5
  interface StreamRenderOptions {
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { o as HtmlResolver } from "../renderer-BdSqllx5.mjs";
2
+ import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
3
3
 
4
4
  //#region src/html/renderers.d.ts
5
5
  declare function dateInputType(format: string | undefined): string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
- import { o as HtmlResolver } from "../renderer-BdSqllx5.mjs";
2
+ import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
3
3
  import { HtmlElement } from "./html.mjs";
4
4
 
5
5
  //#region src/html/streamRenderers.d.ts
@@ -7,10 +7,24 @@ import { ApiLinks } from "./ApiLinks.mjs";
7
7
  import { ApiResponseHeaders } from "./ApiResponseHeaders.mjs";
8
8
  import { ApiSecurity } from "./ApiSecurity.mjs";
9
9
  import { getLinks, getSecurityRequirements, getSecuritySchemes, listCallbacks } from "./parser.mjs";
10
- import { renderField } from "../react/SchemaComponent.mjs";
10
+ import { joinPath, renderField, sanitisePrefix } from "../react/SchemaComponent.mjs";
11
11
  import { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
+ import { useId } from "react";
13
14
  //#region src/openapi/components.tsx
15
+ /**
16
+ * OpenAPI React components with type-safe generics.
17
+ *
18
+ * Render API operations, parameters, request bodies, and response schemas
19
+ * from OpenAPI 3.x documents. When the document is typed `as const`,
20
+ * the `fields` / `overrides` props get full autocomplete.
21
+ *
22
+ * Type safety is enforced at the outer component's props level via
23
+ * conditional types (InferRequestBodyFields, InferResponseFields,
24
+ * InferParameterOverrides). Internally, schemas are extracted and
25
+ * rendered via the walker + headless resolver directly, bypassing
26
+ * SchemaComponent to avoid deferred-conditional-type compatibility issues.
27
+ */
14
28
  function noop() {}
15
29
  function renderSchema(schema, rootDocument, options) {
16
30
  let jsonSchema;
@@ -32,8 +46,11 @@ function renderSchema(schema, rootDocument, options) {
32
46
  rootDocument
33
47
  };
34
48
  const tree = walk(jsonSchema, walkOpts);
35
- const renderChild = (childTree, childValue, childOnChange) => renderField(childTree, childValue, childOnChange, void 0, renderChild, options.widgets);
36
- return renderField(tree, options.value, options.onChange ?? noop, void 0, renderChild, options.widgets);
49
+ const makeRenderChild = (parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
50
+ const childPath = joinPath(parentPath, pathSuffix);
51
+ return renderField(childTree, childValue, childOnChange, void 0, makeRenderChild(childPath), childPath, options.widgets);
52
+ };
53
+ return renderField(tree, options.value, options.onChange ?? noop, void 0, makeRenderChild(options.rootPath), options.rootPath, options.widgets);
37
54
  }
38
55
  function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields, widgets }) {
39
56
  const rootDoc = toDoc(doc);
@@ -42,6 +59,7 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
42
59
  const securityReqs = getSecurityRequirements(parsed, path, method);
43
60
  const securitySchemes = getSecuritySchemes(parsed);
44
61
  const callbacks = listCallbacks(parsed, path, method);
62
+ const instancePrefix = sanitisePrefix(useId());
45
63
  return /* @__PURE__ */ jsxs("section", {
46
64
  "data-operation": `${method.toUpperCase()} ${path}`,
47
65
  children: [
@@ -57,7 +75,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
57
75
  parameters: resolved.parameters,
58
76
  rootDoc,
59
77
  meta,
60
- widgets
78
+ widgets,
79
+ idPrefix: joinPath(instancePrefix, "params")
61
80
  })]
62
81
  }),
63
82
  resolved.requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
@@ -77,7 +96,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
77
96
  onChange: onRequestBodyChange,
78
97
  fields: requestBodyFields,
79
98
  meta,
80
- widgets
99
+ widgets,
100
+ rootPath: joinPath(instancePrefix, "requestBody")
81
101
  })
82
102
  ]
83
103
  }),
@@ -90,7 +110,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
90
110
  meta,
91
111
  widgets,
92
112
  path,
93
- method
113
+ method,
114
+ idPrefix: joinPath(instancePrefix, `response-${response.statusCode}`)
94
115
  }, response.statusCode))]
95
116
  })
96
117
  ]
@@ -99,6 +120,7 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
99
120
  function ApiParameters({ schema: doc, path, method, meta, overrides, widgets }) {
100
121
  const rootDoc = toDoc(doc);
101
122
  const params = resolveParameters(rootDoc, path, method);
123
+ const instancePrefix = sanitisePrefix(useId());
102
124
  if (params.length === 0) return null;
103
125
  return /* @__PURE__ */ jsxs("section", {
104
126
  "data-parameters": true,
@@ -107,13 +129,15 @@ function ApiParameters({ schema: doc, path, method, meta, overrides, widgets })
107
129
  rootDoc,
108
130
  overrides,
109
131
  meta,
110
- widgets
132
+ widgets,
133
+ idPrefix: instancePrefix
111
134
  })]
112
135
  });
113
136
  }
114
137
  function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields, widgets }) {
115
138
  const rootDoc = toDoc(doc);
116
139
  const requestBody = resolveRequestBody(rootDoc, path, method);
140
+ const instancePrefix = sanitisePrefix(useId());
117
141
  if (requestBody?.schema === void 0) return null;
118
142
  return /* @__PURE__ */ jsxs("section", {
119
143
  "data-request-body": true,
@@ -132,7 +156,8 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
132
156
  onChange,
133
157
  fields,
134
158
  meta,
135
- widgets
159
+ widgets,
160
+ rootPath: instancePrefix
136
161
  })
137
162
  ]
138
163
  });
@@ -140,6 +165,7 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
140
165
  function ApiResponse({ schema: doc, path, method, status, value, meta, fields, widgets }) {
141
166
  const rootDoc = toDoc(doc);
142
167
  const response = resolveResponse(rootDoc, path, method, status);
168
+ const instancePrefix = sanitisePrefix(useId());
143
169
  if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
144
170
  "data-status": status,
145
171
  children: [
@@ -156,7 +182,8 @@ function ApiResponse({ schema: doc, path, method, status, value, meta, fields, w
156
182
  meta,
157
183
  widgets,
158
184
  path,
159
- method
185
+ method,
186
+ idPrefix: instancePrefix
160
187
  });
161
188
  }
162
189
  function OperationHeader({ operation }) {
@@ -173,7 +200,7 @@ function OperationHeader({ operation }) {
173
200
  })
174
201
  ] });
175
202
  }
176
- function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
203
+ function ParameterList({ parameters, rootDoc, overrides, meta, widgets, idPrefix }) {
177
204
  return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((param) => /* @__PURE__ */ jsxs("div", {
178
205
  "data-parameter": param.name,
179
206
  children: [
@@ -187,12 +214,13 @@ function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
187
214
  }),
188
215
  renderSchema(param.schema ?? { type: "string" }, rootDoc, {
189
216
  meta: buildParamMeta(param, overrides, meta),
190
- widgets
217
+ widgets,
218
+ rootPath: joinPath(idPrefix, param.name)
191
219
  })
192
220
  ]
193
221
  }, param.name)) });
194
222
  }
195
- function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, method }) {
223
+ function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, method, idPrefix }) {
196
224
  if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
197
225
  "data-status": response.statusCode,
198
226
  children: [
@@ -217,7 +245,8 @@ function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, m
217
245
  readOnly: true,
218
246
  ...meta
219
247
  },
220
- widgets
248
+ widgets,
249
+ rootPath: idPrefix
221
250
  }),
222
251
  /* @__PURE__ */ jsx(ApiResponseHeaders, { headers: response.headers }),
223
252
  /* @__PURE__ */ jsx(ApiLinks, { links })
@@ -1,7 +1,7 @@
1
1
  import { M as WalkedField, T as SchemaMeta, d as FieldOverrides, u as FieldOverride } from "../types-D_5ST7SS.mjs";
2
2
  import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
3
3
  import { t as SchemaError } from "../errors-C5zRC2PU.mjs";
4
- import { l as RenderProps, r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
4
+ import { l as RenderProps, r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
5
5
  import { c as ResolveOpenAPIRef, s as PathOfType, t as FromJSONSchema } from "../typeInference-k7FXfTVO.mjs";
6
6
  import { z } from "zod";
7
7
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
@@ -70,6 +70,13 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
70
70
  description?: string;
71
71
  /** Instance-scoped widgets — override context and global widgets. */
72
72
  widgets?: WidgetMap;
73
+ /**
74
+ * Prefix used for every input `id`/label `htmlFor` in this component
75
+ * subtree. Defaults to a per-instance value from `useId()` so multiple
76
+ * `<SchemaComponent>` instances on the same page never collide. Override
77
+ * for deterministic ids in screenshot tests.
78
+ */
79
+ idPrefix?: string;
73
80
  }
74
81
  declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined>({
75
82
  schema: schemaInput,
@@ -86,9 +93,30 @@ declare function SchemaComponent<T = unknown, Ref extends string | undefined = u
86
93
  readOnly,
87
94
  writeOnly,
88
95
  description,
89
- widgets: instanceWidgets
96
+ widgets: instanceWidgets,
97
+ idPrefix
90
98
  }: SchemaComponentProps<T, Ref>): ReactNode;
91
- declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => ReactNode, instanceWidgets?: WidgetMap, contextWidgets?: WidgetMap, depth?: number): ReactNode;
99
+ /**
100
+ * Default root-path sentinel used when no `idPrefix` is supplied AND the
101
+ * component is rendered outside a React tree (e.g. server-side bundling
102
+ * test harnesses). Production callers receive a `useId()`-derived prefix
103
+ * that is unique per instance.
104
+ */
105
+ declare const ROOT_PATH = "root";
106
+ /**
107
+ * Append a child path suffix to a parent path. When the suffix is omitted
108
+ * (e.g. transparent wrappers like union options), the parent path is
109
+ * returned unchanged so the child inherits the parent's id.
110
+ */
111
+ declare function joinPath(parent: string, suffix: string | undefined): string;
112
+ /**
113
+ * Normalise a `useId()` value into a DOM-id-safe prefix. React's `useId`
114
+ * returns values containing `:` characters (e.g. `«:r0:»`) which are
115
+ * invalid in CSS selectors. Replace any run of non-alphanumeric characters
116
+ * with a single hyphen and trim leading/trailing hyphens.
117
+ */
118
+ declare function sanitisePrefix(value: string): string;
119
+ declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void, pathSuffix?: string) => ReactNode, path: string, instanceWidgets?: WidgetMap, contextWidgets?: WidgetMap, depth?: number): ReactNode;
92
120
  /**
93
121
  * Infer the schema's output type for SchemaField path inference.
94
122
  */
@@ -125,4 +153,4 @@ declare function SchemaField<T = unknown, Ref extends string | undefined = undef
125
153
  onValidationError
126
154
  }: SchemaFieldProps<T, Ref, P>): ReactNode;
127
155
  //#endregion
128
- export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, registerWidget, renderField };
156
+ export { ROOT_PATH, SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, joinPath, registerWidget, renderField, sanitisePrefix };
@@ -8,7 +8,7 @@ import { headlessResolver } from "./headless.mjs";
8
8
  import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
9
9
  import { z } from "zod";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
- import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
11
+ import { createContext, isValidElement, useCallback, useContext, useId, 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,7 +203,7 @@ 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) {
206
+ function buildRenderProps(tree, value, onChange, renderChild, path) {
175
207
  const props = {
176
208
  value,
177
209
  onChange,
@@ -179,7 +211,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
179
211
  writeOnly: tree.editability === "input",
180
212
  meta: tree.meta,
181
213
  constraints: tree.constraints,
182
- path: "",
214
+ path,
183
215
  tree,
184
216
  renderChild
185
217
  };
@@ -203,6 +235,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
203
235
  function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
204
236
  const userResolver = useContext(UserResolverContext);
205
237
  const contextWidgets = useContext(WidgetsContext);
238
+ const generatedId = useId();
206
239
  let jsonSchema;
207
240
  let zodSchema;
208
241
  let rootMeta;
@@ -240,10 +273,12 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
240
273
  onChange,
241
274
  onValidationError
242
275
  ]);
243
- const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
244
- return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1), void 0, contextWidgets, currentDepth + 1);
276
+ const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
277
+ const childPath = joinPath(parentPath, pathSuffix);
278
+ return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, void 0, contextWidgets, currentDepth + 1);
245
279
  };
246
- return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0), void 0, contextWidgets, 0);
280
+ const rootPath = joinPath(sanitisePrefix(generatedId), path);
281
+ return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0, rootPath), rootPath, void 0, contextWidgets, 0);
247
282
  }
248
283
  /**
249
284
  * Dispatch Zod errors to per-field onValidationError callbacks.
@@ -290,4 +325,4 @@ function detectNormalisationKind(err) {
290
325
  return "unknown";
291
326
  }
292
327
  //#endregion
293
- export { SchemaComponent, SchemaField, SchemaProvider, registerWidget, renderField };
328
+ 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
2
  import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
3
- import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
3
+ import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
4
4
  import { WidgetMap } from "./SchemaComponent.mjs";
5
5
  import { ReactNode } from "react";
6
6
 
@@ -30,6 +30,12 @@ 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
41
  * Server-safe schema renderer — no hooks, no context, no state.
@@ -47,7 +53,8 @@ declare function SchemaView({
47
53
  resolver,
48
54
  widgets,
49
55
  onDiagnostic,
50
- strict
56
+ strict,
57
+ idPrefix
51
58
  }: SchemaViewProps): ReactNode;
52
59
  //#endregion
53
60
  export { SchemaView, SchemaViewProps };
@@ -3,8 +3,9 @@ import { normaliseSchema } from "../core/adapter.mjs";
3
3
  import { 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";
6
7
  import { jsx } from "react/jsx-runtime";
7
- import { createElement, isValidElement } from "react";
8
+ import { createElement, isValidElement, useId } from "react";
8
9
  //#region src/react/SchemaView.tsx
9
10
  /**
10
11
  * React Server Component for read-only schema rendering.
@@ -37,7 +38,9 @@ function noop() {}
37
38
  * Always renders in read-only mode. For editable forms, use
38
39
  * `<SchemaComponent>` with `"use client"`.
39
40
  */
40
- function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict }) {
41
+ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict, idPrefix }) {
42
+ const generatedId = useId();
43
+ const rootPath = idPrefix ?? sanitisePrefix(generatedId);
41
44
  const mergedMeta = {
42
45
  ...componentMeta,
43
46
  readOnly: true
@@ -68,18 +71,21 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
68
71
  const tree = walk(jsonSchema, walkOptions);
69
72
  const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
70
73
  const MAX_SERVER_DEPTH = 10;
71
- const makeRenderChild = (currentDepth) => (childTree, childValue) => {
74
+ const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, pathSuffix) => {
75
+ const childPath = joinPath(parentPath, pathSuffix);
72
76
  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);
77
+ return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, widgets);
74
78
  };
75
- const renderChild = makeRenderChild(0);
76
- return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
79
+ const renderChild = makeRenderChild(0, rootPath);
80
+ return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, rootPath, widgets);
77
81
  }
78
- function renderFieldServer(tree, value, resolver, renderChild, widgets) {
82
+ function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
83
+ if (path.length === 0) throw new Error("renderFieldServer requires a non-empty path. Pass ROOT_PATH at the root and join children via joinPath().");
79
84
  const componentHint = tree.meta.component;
80
85
  if (typeof componentHint === "string") {
81
86
  const widget = widgets?.get(componentHint);
82
87
  if (widget !== void 0) {
88
+ const wrapRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
83
89
  const result = widget({
84
90
  value,
85
91
  onChange: noop,
@@ -87,9 +93,9 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
87
93
  writeOnly: false,
88
94
  meta: tree.meta,
89
95
  constraints: tree.constraints,
90
- path: "",
96
+ path,
91
97
  tree,
92
- renderChild: (childTree, childValue) => renderChild(childTree, childValue)
98
+ renderChild: wrapRenderChild
93
99
  });
94
100
  if (result !== void 0 && result !== null) {
95
101
  if (isValidElement(result)) return result;
@@ -106,9 +112,9 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
106
112
  writeOnly: false,
107
113
  meta: tree.meta,
108
114
  constraints: tree.constraints,
109
- path: "",
115
+ path,
110
116
  tree,
111
- renderChild: (childTree, childValue) => renderChild(childTree, childValue)
117
+ renderChild: (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix)
112
118
  };
113
119
  if (tree.type === "enum") props.enumValues = tree.enumValues;
114
120
  if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
1
+ import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
2
2
 
3
3
  //#region src/react/headless.d.ts
4
4
  /**
@@ -1,4 +1,5 @@
1
- import { l as RenderProps } from "../renderer-BdSqllx5.mjs";
1
+ import { M as WalkedField } from "../types-D_5ST7SS.mjs";
2
+ import { l as RenderProps } from "../renderer-B3s8o2B8.mjs";
2
3
  import { ReactNode } from "react";
3
4
 
4
5
  //#region src/react/headlessRenderers.d.ts
@@ -7,17 +8,55 @@ import { ReactNode } from "react";
7
8
  * Returns `null` for unrecognised values.
8
9
  */
9
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;
10
24
  declare function renderString(props: RenderProps): ReactNode;
11
25
  declare function renderNumber(props: RenderProps): ReactNode;
12
26
  declare function renderBoolean(props: RenderProps): ReactNode;
13
27
  declare function renderEnum(props: RenderProps): ReactNode;
14
28
  declare function renderObject(props: RenderProps): ReactNode;
29
+ /**
30
+ * Compute the default value for a freshly added record entry based on the
31
+ * record's value-type schema. Mirrors the read of `defaultValue` used
32
+ * elsewhere in the renderer, falling back to a type-appropriate empty value.
33
+ */
34
+ declare function defaultRecordValue(valueType: WalkedField): unknown;
35
+ /**
36
+ * Generate a unique, currently-unused key for a new record entry.
37
+ * Picks the first of `key`, `key-1`, `key-2`, … that is not in `existing`.
38
+ */
39
+ declare function nextRecordKey(existing: readonly string[], base?: string): string;
40
+ /**
41
+ * Rename a key in an object while preserving insertion order. Returns the
42
+ * original object reference when the rename is a no-op (oldKey === newKey)
43
+ * or when newKey collides with an existing key.
44
+ */
45
+ declare function renameRecordKey(obj: Record<string, unknown>, oldKey: string, newKey: string): Record<string, unknown>;
15
46
  declare function renderRecord(props: RenderProps): ReactNode;
16
47
  declare function renderArray(props: RenderProps): ReactNode;
17
48
  declare function renderUnion(props: RenderProps): ReactNode;
18
49
  declare function renderDiscriminatedUnion(props: RenderProps): ReactNode;
50
+ /**
51
+ * Pure helper: convert a tab index into the new value the discriminated
52
+ * union should emit. Returns `undefined` when the index is out of bounds.
53
+ *
54
+ * Extracted from `DiscriminatedUnionTabs` so the contract is unit-testable
55
+ * without rendering the tabs component (which relies on React hooks).
56
+ */
57
+ declare function discriminatedUnionValueForTab(optionLabels: readonly string[], discKey: string, newIndex: number): Record<string, string> | undefined;
19
58
  declare function renderFile(props: RenderProps): ReactNode;
20
59
  declare function renderRecursive(props: RenderProps): ReactNode;
21
60
  declare function renderUnknown(props: RenderProps): ReactNode;
22
61
  //#endregion
23
- export { 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 };