schema-components 1.3.2 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [1.4.0](https://github.com/Mearman/schema-components/compare/v1.3.3...v1.4.0) (2026-05-14)
2
+
3
+ ### Features
4
+
5
+ * add scoped widget resolution at instance, context, and global levels ([b330baf](https://github.com/Mearman/schema-components/commit/b330baf183d00bebf999108f16c9dabd40c243be))
6
+
7
+ ### Documentation
8
+
9
+ * update README with scoped widget resolution documentation ([21ac50d](https://github.com/Mearman/schema-components/commit/21ac50dc9a55144fc319fcf482ffad6a16585a27))
10
+
11
+ ## [1.3.3](https://github.com/Mearman/schema-components/compare/v1.3.2...v1.3.3) (2026-05-14)
12
+
13
+ ### Refactoring
14
+
15
+ * extract pure resolution layer from openapi components ([5e0ab32](https://github.com/Mearman/schema-components/commit/5e0ab32de44b41b23e74082886a5ae881b418883))
16
+
17
+ ### Tests
18
+
19
+ * add guards unit tests ([39f53ab](https://github.com/Mearman/schema-components/commit/39f53abf4cf30f1751a09267f34990c4a2532567))
20
+ * add openapi resolution and component unit tests ([75a5892](https://github.com/Mearman/schema-components/commit/75a589220f05b5649a531bec946a63093e824687))
21
+ * add renderer utility unit tests ([3ca3677](https://github.com/Mearman/schema-components/commit/3ca367709acf6324a33a79adf3799528971defc2))
22
+ * expand html and streaming coverage, tighten vitest config ([ac480cd](https://github.com/Mearman/schema-components/commit/ac480cdeb39cf2ae0fa3107de2abae39102cfa26))
23
+
1
24
  ## [1.3.2](https://github.com/Mearman/schema-components/compare/v1.3.1...v1.3.2) (2026-05-14)
2
25
 
3
26
  ### Refactoring
package/README.md CHANGED
@@ -434,7 +434,15 @@ Custom resolvers fall back to the default for any type you don't override.
434
434
 
435
435
  ## Custom widgets
436
436
 
437
- Register widgets by `.meta({ component })` hint:
437
+ Widgets let you override rendering for specific fields using `.meta({ component: name })`. Three scopes are available, checked in order:
438
+
439
+ 1. **Instance** — `widgets` prop on `<SchemaComponent>`
440
+ 2. **Context** — `widgets` prop on `<SchemaProvider>`
441
+ 3. **Global** — `registerWidget()` for app-wide defaults
442
+
443
+ If none match, the theme adapter or headless default handles the field.
444
+
445
+ ### Global registration
438
446
 
439
447
  ```tsx
440
448
  import { registerWidget } from "schema-components/react/SchemaComponent";
@@ -443,13 +451,65 @@ registerWidget("richtext", ({ value, onChange }) => (
443
451
  <RichTextEditor value={value} onChange={onChange} />
444
452
  ));
445
453
 
446
- // In schema
447
454
  const schema = z.object({
448
455
  bio: z.string().meta({ component: "richtext" }),
449
456
  });
450
457
  ```
451
458
 
452
- Resolution order: `.meta({ component })` → registered widget → theme adapter → headless default.
459
+ ### Context-scoped widgets
460
+
461
+ Share widgets across a subtree via `<SchemaProvider>`:
462
+
463
+ ```tsx
464
+ import { SchemaProvider } from "schema-components/react/SchemaComponent";
465
+ import type { WidgetMap } from "schema-components/react/SchemaComponent";
466
+
467
+ const adminWidgets: WidgetMap = new Map([
468
+ ["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
469
+ ["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
470
+ ]);
471
+
472
+ <SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
473
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
474
+ <SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
475
+ </SchemaProvider>
476
+ ```
477
+
478
+ ### Instance-scoped widgets
479
+
480
+ Override widgets for a single form:
481
+
482
+ ```tsx
483
+ const formWidgets: WidgetMap = new Map([
484
+ ["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
485
+ ]);
486
+
487
+ <SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />
488
+ ```
489
+
490
+ ### Resolution order
491
+
492
+ ```.meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default
493
+ ```
494
+
495
+ Instance overrides context. Context overrides global. Unhinted fields skip the widget layer entirely.
496
+
497
+ ### `WidgetMap` type
498
+
499
+ ```tsx
500
+ import type { WidgetMap } from "schema-components/react/SchemaComponent";
501
+
502
+ // ReadonlyMap<string, (props: RenderProps) => unknown>
503
+ const widgets: WidgetMap = new Map([
504
+ ["name", (props) => <MyInput {...props} />],
505
+ ]);
506
+ ```
507
+
508
+ Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React context available):
509
+
510
+ ```tsx
511
+ <SchemaView schema={schema} value={data} widgets={serverWidgets} />
512
+ ```
453
513
 
454
514
  ## Validation
455
515
 
@@ -1,4 +1,5 @@
1
1
  import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-DDCD6Xnx.mjs";
2
+ import { WidgetMap } from "../react/SchemaComponent.mjs";
2
3
  import { ReactNode } from "react";
3
4
 
4
5
  //#region src/openapi/components.d.ts
@@ -11,6 +12,8 @@ interface ApiOperationProps<Doc = unknown, Path extends string = string, Method
11
12
  responseValue?: unknown;
12
13
  meta?: SchemaMeta;
13
14
  requestBodyFields?: Doc extends Record<string, unknown> ? InferRequestBodyFields<Doc, Path, Method> : Record<string, FieldOverride>;
15
+ /** Instance-scoped widgets. */
16
+ widgets?: WidgetMap;
14
17
  }
15
18
  declare function ApiOperation<Doc = unknown, Path extends string = string, Method extends string = string>({
16
19
  schema: doc,
@@ -20,7 +23,8 @@ declare function ApiOperation<Doc = unknown, Path extends string = string, Metho
20
23
  onRequestBodyChange,
21
24
  responseValue,
22
25
  meta,
23
- requestBodyFields
26
+ requestBodyFields,
27
+ widgets
24
28
  }: ApiOperationProps<Doc, Path, Method>): ReactNode;
25
29
  interface ApiParametersProps<Doc = unknown, Path extends string = string, Method extends string = string> {
26
30
  schema: Doc;
@@ -28,13 +32,16 @@ interface ApiParametersProps<Doc = unknown, Path extends string = string, Method
28
32
  method: Method;
29
33
  meta?: SchemaMeta;
30
34
  overrides?: Doc extends Record<string, unknown> ? InferParameterOverrides<Doc, Path, Method> : Record<string, FieldOverride>;
35
+ /** Instance-scoped widgets. */
36
+ widgets?: WidgetMap;
31
37
  }
32
38
  declare function ApiParameters<Doc = unknown, Path extends string = string, Method extends string = string>({
33
39
  schema: doc,
34
40
  path,
35
41
  method,
36
42
  meta,
37
- overrides
43
+ overrides,
44
+ widgets
38
45
  }: ApiParametersProps<Doc, Path, Method>): ReactNode;
39
46
  interface ApiRequestBodyProps<Doc = unknown, Path extends string = string, Method extends string = string> {
40
47
  schema: Doc;
@@ -44,6 +51,8 @@ interface ApiRequestBodyProps<Doc = unknown, Path extends string = string, Metho
44
51
  onChange?: (value: unknown) => void;
45
52
  meta?: SchemaMeta;
46
53
  fields?: Doc extends Record<string, unknown> ? InferRequestBodyFields<Doc, Path, Method> : Record<string, FieldOverride>;
54
+ /** Instance-scoped widgets. */
55
+ widgets?: WidgetMap;
47
56
  }
48
57
  declare function ApiRequestBody<Doc = unknown, Path extends string = string, Method extends string = string>({
49
58
  schema: doc,
@@ -52,7 +61,8 @@ declare function ApiRequestBody<Doc = unknown, Path extends string = string, Met
52
61
  value,
53
62
  onChange,
54
63
  meta,
55
- fields
64
+ fields,
65
+ widgets
56
66
  }: ApiRequestBodyProps<Doc, Path, Method>): ReactNode;
57
67
  interface ApiResponseProps<Doc = unknown, Path extends string = string, Method extends string = string, Status extends string = string> {
58
68
  schema: Doc;
@@ -62,6 +72,8 @@ interface ApiResponseProps<Doc = unknown, Path extends string = string, Method e
62
72
  value?: unknown;
63
73
  meta?: SchemaMeta;
64
74
  fields?: Doc extends Record<string, unknown> ? InferResponseFields<Doc, Path, Method, Status> : Record<string, FieldOverride>;
75
+ /** Instance-scoped widgets. */
76
+ widgets?: WidgetMap;
65
77
  }
66
78
  declare function ApiResponse<Doc = unknown, Path extends string = string, Method extends string = string, Status extends string = string>({
67
79
  schema: doc,
@@ -70,7 +82,8 @@ declare function ApiResponse<Doc = unknown, Path extends string = string, Method
70
82
  status,
71
83
  value,
72
84
  meta,
73
- fields
85
+ fields,
86
+ widgets
74
87
  }: ApiResponseProps<Doc, Path, Method, Status>): ReactNode;
75
88
  //#endregion
76
89
  export { ApiOperation, ApiOperationProps, ApiParameters, ApiParametersProps, ApiRequestBody, ApiRequestBodyProps, ApiResponse, ApiResponseProps };
@@ -1,23 +1,12 @@
1
- import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
1
+ import { toRecordOrUndefined } from "../core/guards.mjs";
2
2
  import { normaliseSchema } from "../core/adapter.mjs";
3
3
  import { SchemaNormalisationError } from "../core/errors.mjs";
4
4
  import { walk } from "../core/walker.mjs";
5
- import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
6
5
  import { renderField } from "../react/SchemaComponent.mjs";
6
+ import { resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/openapi/components.tsx
9
- const docCache = /* @__PURE__ */ new WeakMap();
10
- function getParsed(doc) {
11
- const cached = docCache.get(doc);
12
- if (cached !== void 0) return cached;
13
- const parsed = parseOpenApiDocument(doc);
14
- docCache.set(doc, parsed);
15
- return parsed;
16
- }
17
9
  function noop() {}
18
- function toDoc(value) {
19
- return isObject(value) ? value : {};
20
- }
21
10
  function renderSchema(schema, rootDocument, options) {
22
11
  let jsonSchema;
23
12
  let rootMeta;
@@ -38,65 +27,62 @@ function renderSchema(schema, rootDocument, options) {
38
27
  rootDocument
39
28
  };
40
29
  const tree = walk(jsonSchema, walkOpts);
41
- const renderChild = (childTree, childValue, childOnChange) => renderField(childTree, childValue, childOnChange, void 0, renderChild);
42
- return renderField(tree, options.value, options.onChange ?? noop, void 0, renderChild);
30
+ const renderChild = (childTree, childValue, childOnChange) => renderField(childTree, childValue, childOnChange, void 0, renderChild, options.widgets);
31
+ return renderField(tree, options.value, options.onChange ?? noop, void 0, renderChild, options.widgets);
43
32
  }
44
- function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields }) {
45
- const parsed = getParsed(toDoc(doc));
33
+ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields, widgets }) {
46
34
  const rootDoc = toDoc(doc);
47
- const operation = listOperations(parsed).find((op) => op.path === path && op.method === method);
48
- if (operation === void 0) throw new SchemaNormalisationError(`Operation not found: ${method.toUpperCase()} ${path}`, doc, "openapi-invalid");
49
- const params = getParameters(parsed, path, method);
50
- const requestBody = getRequestBody(parsed, path, method);
51
- const responses = getResponses(parsed, path, method);
35
+ const resolved = resolveOperation(rootDoc, path, method);
52
36
  return /* @__PURE__ */ jsxs("section", {
53
37
  "data-operation": `${method.toUpperCase()} ${path}`,
54
38
  children: [
55
- /* @__PURE__ */ jsx(OperationHeader, { operation }),
56
- params.length > 0 && /* @__PURE__ */ jsxs("section", {
39
+ /* @__PURE__ */ jsx(OperationHeader, { operation: resolved.operation }),
40
+ resolved.parameters.length > 0 && /* @__PURE__ */ jsxs("section", {
57
41
  "data-parameters": true,
58
42
  children: [/* @__PURE__ */ jsx("h4", { children: "Parameters" }), /* @__PURE__ */ jsx(ParameterList, {
59
- parameters: params,
43
+ parameters: resolved.parameters,
60
44
  rootDoc,
61
- meta
45
+ meta,
46
+ widgets
62
47
  })]
63
48
  }),
64
- requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
49
+ resolved.requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
65
50
  "data-request-body": true,
66
51
  children: [
67
- /* @__PURE__ */ jsxs("h4", { children: ["Request Body", requestBody.required && /* @__PURE__ */ jsx("span", {
52
+ /* @__PURE__ */ jsxs("h4", { children: ["Request Body", resolved.requestBody.required && /* @__PURE__ */ jsx("span", {
68
53
  "data-required": true,
69
54
  children: "*"
70
55
  })] }),
71
- requestBody.description && /* @__PURE__ */ jsx("p", { children: requestBody.description }),
72
- requestBody.contentTypes.length > 0 && /* @__PURE__ */ jsx("span", {
56
+ resolved.requestBody.description && /* @__PURE__ */ jsx("p", { children: resolved.requestBody.description }),
57
+ resolved.requestBody.contentTypes.length > 0 && /* @__PURE__ */ jsx("span", {
73
58
  "data-content-type": true,
74
- children: requestBody.contentTypes[0]
59
+ children: resolved.requestBody.contentTypes[0]
75
60
  }),
76
- renderSchema(requestBody.schema, rootDoc, {
61
+ renderSchema(resolved.requestBody.schema, rootDoc, {
77
62
  value: requestBodyValue,
78
63
  onChange: onRequestBodyChange,
79
64
  fields: requestBodyFields,
80
- meta
65
+ meta,
66
+ widgets
81
67
  })
82
68
  ]
83
69
  }),
84
- responses.length > 0 && /* @__PURE__ */ jsxs("section", {
70
+ resolved.responses.length > 0 && /* @__PURE__ */ jsxs("section", {
85
71
  "data-responses": true,
86
- children: [/* @__PURE__ */ jsx("h4", { children: "Responses" }), responses.map((response) => /* @__PURE__ */ jsx(ResponseCard, {
72
+ children: [/* @__PURE__ */ jsx("h4", { children: "Responses" }), resolved.responses.map((response) => /* @__PURE__ */ jsx(ResponseCard, {
87
73
  response,
88
74
  rootDoc,
89
75
  value: responseValue,
90
- meta
76
+ meta,
77
+ widgets
91
78
  }, response.statusCode))]
92
79
  })
93
80
  ]
94
81
  });
95
82
  }
96
- function ApiParameters({ schema: doc, path, method, meta, overrides }) {
97
- const parsed = getParsed(toDoc(doc));
83
+ function ApiParameters({ schema: doc, path, method, meta, overrides, widgets }) {
98
84
  const rootDoc = toDoc(doc);
99
- const params = getParameters(parsed, path, method);
85
+ const params = resolveParameters(rootDoc, path, method);
100
86
  if (params.length === 0) return null;
101
87
  return /* @__PURE__ */ jsxs("section", {
102
88
  "data-parameters": true,
@@ -104,14 +90,14 @@ function ApiParameters({ schema: doc, path, method, meta, overrides }) {
104
90
  parameters: params,
105
91
  rootDoc,
106
92
  overrides,
107
- meta
93
+ meta,
94
+ widgets
108
95
  })]
109
96
  });
110
97
  }
111
- function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields }) {
112
- const parsed = getParsed(toDoc(doc));
98
+ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields, widgets }) {
113
99
  const rootDoc = toDoc(doc);
114
- const requestBody = getRequestBody(parsed, path, method);
100
+ const requestBody = resolveRequestBody(rootDoc, path, method);
115
101
  if (requestBody?.schema === void 0) return null;
116
102
  return /* @__PURE__ */ jsxs("section", {
117
103
  "data-request-body": true,
@@ -129,16 +115,15 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
129
115
  value,
130
116
  onChange,
131
117
  fields,
132
- meta
118
+ meta,
119
+ widgets
133
120
  })
134
121
  ]
135
122
  });
136
123
  }
137
- function ApiResponse({ schema: doc, path, method, status, value, meta, fields }) {
138
- const parsed = getParsed(toDoc(doc));
124
+ function ApiResponse({ schema: doc, path, method, status, value, meta, fields, widgets }) {
139
125
  const rootDoc = toDoc(doc);
140
- const response = getResponses(parsed, path, method).find((r) => r.statusCode === status);
141
- if (response === void 0) throw new SchemaNormalisationError(`Response not found: ${status}`, doc, "openapi-invalid");
126
+ const response = resolveResponse(rootDoc, path, method, status);
142
127
  if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
143
128
  "data-status": status,
144
129
  children: [
@@ -152,7 +137,8 @@ function ApiResponse({ schema: doc, path, method, status, value, meta, fields })
152
137
  rootDoc,
153
138
  value,
154
139
  fields,
155
- meta
140
+ meta,
141
+ widgets
156
142
  });
157
143
  }
158
144
  function OperationHeader({ operation }) {
@@ -169,7 +155,7 @@ function OperationHeader({ operation }) {
169
155
  })
170
156
  ] });
171
157
  }
172
- function ParameterList({ parameters, rootDoc, overrides, meta }) {
158
+ function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
173
159
  return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((param) => /* @__PURE__ */ jsxs("div", {
174
160
  "data-parameter": param.name,
175
161
  children: [
@@ -181,11 +167,14 @@ function ParameterList({ parameters, rootDoc, overrides, meta }) {
181
167
  "data-description": true,
182
168
  children: param.description
183
169
  }),
184
- renderSchema(param.schema ?? { type: "string" }, rootDoc, { meta: buildParamMeta(param, overrides, meta) })
170
+ renderSchema(param.schema ?? { type: "string" }, rootDoc, {
171
+ meta: buildParamMeta(param, overrides, meta),
172
+ widgets
173
+ })
185
174
  ]
186
175
  }, param.name)) });
187
176
  }
188
- function ResponseCard({ response, rootDoc, value, fields, meta }) {
177
+ function ResponseCard({ response, rootDoc, value, fields, meta, widgets }) {
189
178
  if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
190
179
  "data-status": response.statusCode,
191
180
  children: [
@@ -205,7 +194,8 @@ function ResponseCard({ response, rootDoc, value, fields, meta }) {
205
194
  meta: {
206
195
  readOnly: true,
207
196
  ...meta
208
- }
197
+ },
198
+ widgets
209
199
  })
210
200
  ]
211
201
  });
@@ -0,0 +1,42 @@
1
+ import { OpenApiDocument, OperationInfo, ParameterInfo, ResponseInfo, getRequestBody } from "./parser.mjs";
2
+
3
+ //#region src/openapi/resolve.d.ts
4
+ /**
5
+ * Parse and cache an OpenAPI document. Returns cached version if
6
+ * the same object identity has been seen before.
7
+ */
8
+ declare function getParsed(doc: Record<string, unknown>): OpenApiDocument;
9
+ /**
10
+ * Coerce an unknown value to a record, returning an empty record
11
+ * for non-objects.
12
+ */
13
+ declare function toDoc(value: unknown): Record<string, unknown>;
14
+ interface ResolvedOperation {
15
+ operation: OperationInfo;
16
+ parameters: ParameterInfo[];
17
+ requestBody: ReturnType<typeof getRequestBody>;
18
+ responses: ResponseInfo[];
19
+ }
20
+ /**
21
+ * Resolve an operation from an OpenAPI document by path and method.
22
+ * Throws if the operation is not found.
23
+ */
24
+ declare function resolveOperation(doc: Record<string, unknown>, path: string, method: string): ResolvedOperation;
25
+ /**
26
+ * Resolve parameters for an operation. Returns empty array if none.
27
+ */
28
+ declare function resolveParameters(doc: Record<string, unknown>, path: string, method: string): ParameterInfo[];
29
+ /**
30
+ * Resolve request body for an operation. Returns undefined if none.
31
+ */
32
+ declare function resolveRequestBody(doc: Record<string, unknown>, path: string, method: string): ReturnType<typeof getRequestBody>;
33
+ /**
34
+ * Resolve a specific response by status code. Throws if not found.
35
+ */
36
+ declare function resolveResponse(doc: Record<string, unknown>, path: string, method: string, statusCode: string): ResponseInfo;
37
+ /**
38
+ * Resolve all responses for an operation.
39
+ */
40
+ declare function resolveResponses(doc: Record<string, unknown>, path: string, method: string): ResponseInfo[];
41
+ //#endregion
42
+ export { ResolvedOperation, getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, resolveResponses, toDoc };
@@ -0,0 +1,72 @@
1
+ import { isObject } from "../core/guards.mjs";
2
+ import { getParameters, getRequestBody, getResponses, listOperations, parseOpenApiDocument } from "./parser.mjs";
3
+ //#region src/openapi/resolve.ts
4
+ /**
5
+ * OpenAPI document resolution and caching.
6
+ *
7
+ * Pure functions for looking up operations, parameters, request bodies,
8
+ * and responses from parsed OpenAPI documents. Extracted from components
9
+ * for testability without React.
10
+ */
11
+ const docCache = /* @__PURE__ */ new WeakMap();
12
+ /**
13
+ * Parse and cache an OpenAPI document. Returns cached version if
14
+ * the same object identity has been seen before.
15
+ */
16
+ function getParsed(doc) {
17
+ const cached = docCache.get(doc);
18
+ if (cached !== void 0) return cached;
19
+ const parsed = parseOpenApiDocument(doc);
20
+ docCache.set(doc, parsed);
21
+ return parsed;
22
+ }
23
+ /**
24
+ * Coerce an unknown value to a record, returning an empty record
25
+ * for non-objects.
26
+ */
27
+ function toDoc(value) {
28
+ return isObject(value) ? value : {};
29
+ }
30
+ /**
31
+ * Resolve an operation from an OpenAPI document by path and method.
32
+ * Throws if the operation is not found.
33
+ */
34
+ function resolveOperation(doc, path, method) {
35
+ const parsed = getParsed(doc);
36
+ const operation = listOperations(parsed).find((op) => op.path === path && op.method === method);
37
+ if (operation === void 0) throw new Error(`Operation not found: ${method.toUpperCase()} ${path}`);
38
+ return {
39
+ operation,
40
+ parameters: getParameters(parsed, path, method),
41
+ requestBody: getRequestBody(parsed, path, method),
42
+ responses: getResponses(parsed, path, method)
43
+ };
44
+ }
45
+ /**
46
+ * Resolve parameters for an operation. Returns empty array if none.
47
+ */
48
+ function resolveParameters(doc, path, method) {
49
+ return getParameters(getParsed(doc), path, method);
50
+ }
51
+ /**
52
+ * Resolve request body for an operation. Returns undefined if none.
53
+ */
54
+ function resolveRequestBody(doc, path, method) {
55
+ return getRequestBody(getParsed(doc), path, method);
56
+ }
57
+ /**
58
+ * Resolve a specific response by status code. Throws if not found.
59
+ */
60
+ function resolveResponse(doc, path, method, statusCode) {
61
+ const response = getResponses(getParsed(doc), path, method).find((r) => r.statusCode === statusCode);
62
+ if (response === void 0) throw new Error(`Response not found: ${statusCode}`);
63
+ return response;
64
+ }
65
+ /**
66
+ * Resolve all responses for an operation.
67
+ */
68
+ function resolveResponses(doc, path, method) {
69
+ return getResponses(getParsed(doc), path, method);
70
+ }
71
+ //#endregion
72
+ export { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, resolveResponses, toDoc };
@@ -5,13 +5,33 @@ import { ReactNode } from "react";
5
5
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
6
6
 
7
7
  //#region src/react/SchemaComponent.d.ts
8
+ /**
9
+ * Widget map — maps component hints (from `.meta({ component })`) to render
10
+ * functions. Scoped at three levels:
11
+ *
12
+ * 1. **Per-instance** — `widgets` prop on `<SchemaComponent>`
13
+ * 2. **Context-scoped** — `widgets` prop on `<SchemaProvider>`
14
+ * 3. **Global** — `registerWidget()` (app-wide defaults)
15
+ *
16
+ * Resolution order: instance → context → global → resolver → headless.
17
+ */
18
+ type WidgetMap = ReadonlyMap<string, (props: RenderProps) => unknown>;
8
19
  declare function SchemaProvider({
9
20
  resolver,
21
+ widgets,
10
22
  children
11
23
  }: {
12
- resolver: ComponentResolver;
24
+ resolver: ComponentResolver; /** Scoped widgets available to all SchemaComponents in this subtree. */
25
+ widgets?: WidgetMap;
13
26
  children: ReactNode;
14
27
  }): _$react_jsx_runtime0.JSX.Element;
28
+ /**
29
+ * Register a widget globally. The widget is resolved when a schema field
30
+ * has `.meta({ component: name })`.
31
+ *
32
+ * For scoped registration, use the `widgets` prop on `<SchemaComponent>`
33
+ * or `<SchemaProvider>` instead.
34
+ */
15
35
  declare function registerWidget(name: string, render: (props: RenderProps) => unknown): void;
16
36
  type InferFields<T, Ref extends string | undefined> = T extends z.ZodType ? FieldOverrides<z.infer<T>> : T extends {
17
37
  openapi: unknown;
@@ -41,6 +61,8 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
41
61
  writeOnly?: boolean;
42
62
  /** Convenience: sets description on the root. */
43
63
  description?: string;
64
+ /** Instance-scoped widgets — override context and global widgets. */
65
+ widgets?: WidgetMap;
44
66
  }
45
67
  declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined>({
46
68
  schema: schemaInput,
@@ -54,9 +76,10 @@ declare function SchemaComponent<T = unknown, Ref extends string | undefined = u
54
76
  meta: componentMeta,
55
77
  readOnly,
56
78
  writeOnly,
57
- description
79
+ description,
80
+ widgets: instanceWidgets
58
81
  }: SchemaComponentProps<T, Ref>): ReactNode;
59
- declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => ReactNode): ReactNode;
82
+ 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): ReactNode;
60
83
  /**
61
84
  * Infer the schema's output type for SchemaField path inference.
62
85
  */
@@ -93,4 +116,4 @@ declare function SchemaField<T = unknown, Ref extends string | undefined = undef
93
116
  onValidationError
94
117
  }: SchemaFieldProps<T, Ref, P>): ReactNode;
95
118
  //#endregion
96
- export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, registerWidget, renderField };
119
+ export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, registerWidget, renderField };
@@ -23,18 +23,31 @@ import { jsx } from "react/jsx-runtime";
23
23
  * - Runtime schemas → Record<string, FieldOverride> (no autocomplete)
24
24
  */
25
25
  const UserResolverContext = createContext(void 0);
26
- function SchemaProvider({ resolver, children }) {
26
+ const WidgetsContext = createContext(void 0);
27
+ function SchemaProvider({ resolver, widgets, children }) {
27
28
  return /* @__PURE__ */ jsx(UserResolverContext.Provider, {
28
29
  value: resolver,
29
- children
30
+ children: /* @__PURE__ */ jsx(WidgetsContext.Provider, {
31
+ value: widgets,
32
+ children
33
+ })
30
34
  });
31
35
  }
32
- const widgetRegistry = /* @__PURE__ */ new Map();
36
+ /** Global widget registry app-wide defaults. */
37
+ const globalWidgets = /* @__PURE__ */ new Map();
38
+ /**
39
+ * Register a widget globally. The widget is resolved when a schema field
40
+ * has `.meta({ component: name })`.
41
+ *
42
+ * For scoped registration, use the `widgets` prop on `<SchemaComponent>`
43
+ * or `<SchemaProvider>` instead.
44
+ */
33
45
  function registerWidget(name, render) {
34
- widgetRegistry.set(name, render);
46
+ globalWidgets.set(name, render);
35
47
  }
36
- function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, fields, meta: componentMeta, readOnly, writeOnly, description }) {
48
+ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets }) {
37
49
  const userResolver = useContext(UserResolverContext);
50
+ const contextWidgets = useContext(WidgetsContext);
38
51
  const mergedMeta = useMemo(() => {
39
52
  const merged = { ...componentMeta };
40
53
  if (readOnly === true) merged.readOnly = true;
@@ -82,9 +95,9 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
82
95
  rootDocument
83
96
  });
84
97
  const renderChild = (childTree, childValue, childOnChange) => {
85
- return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
98
+ return renderField(childTree, childValue, childOnChange, userResolver, renderChild, instanceWidgets, contextWidgets);
86
99
  };
87
- return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild);
100
+ return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets);
88
101
  }
89
102
  function runValidation(zodSchema, jsonSchema, value, onError) {
90
103
  if (zodSchema !== void 0 && isObject(zodSchema)) {
@@ -107,10 +120,10 @@ function runValidation(zodSchema, jsonSchema, value, onError) {
107
120
  }
108
121
  }
109
122
  }
110
- function renderField(tree, value, onChange, userResolver, renderChild) {
123
+ function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets) {
111
124
  const componentHint = tree.meta.component;
112
125
  if (typeof componentHint === "string") {
113
- const widget = widgetRegistry.get(componentHint);
126
+ const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
114
127
  if (widget !== void 0) {
115
128
  const result = widget(buildRenderProps(tree, value, onChange, renderChild));
116
129
  if (result !== void 0 && result !== null) {
@@ -160,6 +173,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
160
173
  }
161
174
  function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
162
175
  const userResolver = useContext(UserResolverContext);
176
+ const contextWidgets = useContext(WidgetsContext);
163
177
  let jsonSchema;
164
178
  let zodSchema;
165
179
  let rootMeta;
@@ -197,9 +211,9 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
197
211
  onValidationError
198
212
  ]);
199
213
  const renderChild = (childTree, childValue, childOnChange) => {
200
- return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
214
+ return renderField(childTree, childValue, childOnChange, userResolver, renderChild, void 0, contextWidgets);
201
215
  };
202
- return renderField(fieldTree, fieldValue, handleChange, userResolver, renderChild);
216
+ return renderField(fieldTree, fieldValue, handleChange, userResolver, renderChild, void 0, contextWidgets);
203
217
  }
204
218
  function resolvePath(tree, path) {
205
219
  if (path.length === 0) return tree;
@@ -1,4 +1,5 @@
1
1
  import { b as ComponentResolver, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
2
+ import { WidgetMap } from "./SchemaComponent.mjs";
2
3
  import { ReactNode } from "react";
3
4
 
4
5
  //#region src/react/SchemaView.d.ts
@@ -21,6 +22,8 @@ interface SchemaViewProps {
21
22
  * Falls back to the headless resolver if omitted.
22
23
  */
23
24
  resolver?: ComponentResolver;
25
+ /** Instance-scoped widgets. */
26
+ widgets?: WidgetMap;
24
27
  }
25
28
  /**
26
29
  * Server-safe schema renderer — no hooks, no context, no state.
@@ -35,7 +38,8 @@ declare function SchemaView({
35
38
  fields,
36
39
  meta: componentMeta,
37
40
  description,
38
- resolver
41
+ resolver,
42
+ widgets
39
43
  }: SchemaViewProps): ReactNode;
40
44
  //#endregion
41
45
  export { SchemaView, SchemaViewProps };
@@ -37,7 +37,7 @@ function noop() {}
37
37
  * Always renders in read-only mode. For editable forms, use
38
38
  * `<SchemaComponent>` with `"use client"`.
39
39
  */
40
- function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver }) {
40
+ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets }) {
41
41
  const mergedMeta = {
42
42
  ...componentMeta,
43
43
  readOnly: true
@@ -61,10 +61,31 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
61
61
  rootDocument
62
62
  });
63
63
  const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
64
- const renderChild = (childTree, childValue) => renderFieldServer(childTree, childValue, userResolver, renderChild);
65
- return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild);
64
+ const renderChild = (childTree, childValue) => renderFieldServer(childTree, childValue, userResolver, renderChild, widgets);
65
+ return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
66
66
  }
67
- function renderFieldServer(tree, value, resolver, renderChild) {
67
+ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
68
+ const componentHint = tree.meta.component;
69
+ if (typeof componentHint === "string") {
70
+ const widget = widgets?.get(componentHint);
71
+ if (widget !== void 0) {
72
+ const result = widget({
73
+ value,
74
+ onChange: noop,
75
+ readOnly: true,
76
+ writeOnly: false,
77
+ meta: tree.meta,
78
+ constraints: tree.constraints,
79
+ path: "",
80
+ tree,
81
+ renderChild: (childTree, childValue) => renderChild(childTree, childValue)
82
+ });
83
+ if (result !== void 0 && result !== null) {
84
+ if (isValidElement(result)) return result;
85
+ if (typeof result === "string" || typeof result === "number") return result;
86
+ }
87
+ }
88
+ }
68
89
  const renderFn = getRenderFunction(tree.type, resolver);
69
90
  if (renderFn !== void 0) {
70
91
  const props = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",