schema-components 1.5.1 → 1.7.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,19 @@
1
+ ## [1.7.0](https://github.com/Mearman/schema-components/compare/v1.6.0...v1.7.0) (2026-05-14)
2
+
3
+ ### Features
4
+
5
+ * add field visibility and ordering controls ([27cb3e1](https://github.com/Mearman/schema-components/commit/27cb3e19ba930200116c79293078fa6fa2743723))
6
+
7
+ ## [1.6.0](https://github.com/Mearman/schema-components/compare/v1.5.1...v1.6.0) (2026-05-14)
8
+
9
+ ### Features
10
+
11
+ * add per-field onValidationError and writeOnly tests ([bc65589](https://github.com/Mearman/schema-components/commit/bc655893601b8e015dd3c717eccc9bf9dffbbb42))
12
+
13
+ ### Tests
14
+
15
+ * add writeOnly behaviour tests across all renderers ([abfb293](https://github.com/Mearman/schema-components/commit/abfb293498efc4d4095dec072fd1e5a9c3239035))
16
+
1
17
  ## [1.5.1](https://github.com/Mearman/schema-components/compare/v1.5.0...v1.5.1) (2026-05-14)
2
18
 
3
19
  ### Bug Fixes
@@ -1,4 +1,4 @@
1
- import { l as JsonObject, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { l as JsonObject, m as SchemaMeta } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/core/adapter.d.ts
4
4
  type SchemaInput = Record<string, unknown>;
@@ -1,2 +1,2 @@
1
- import { A as mergeResolvers, C as HtmlResolver, D as getHtmlRenderFn, E as RenderProps, O as getRenderFunction, S as HtmlRenderProps, T as RenderFunction, b as ComponentResolver, j as typeToKey, k as mergeHtmlResolvers, w as RESOLVER_KEYS, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-DDCD6Xnx.mjs";
1
+ import { A as mergeResolvers, C as HtmlResolver, D as getHtmlRenderFn, E as RenderProps, O as getRenderFunction, S as HtmlRenderProps, T as RenderFunction, b as ComponentResolver, j as typeToKey, k as mergeHtmlResolvers, w as RESOLVER_KEYS, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-BJzEgJdX.mjs";
2
2
  export { BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
@@ -1,3 +1,3 @@
1
- import { C as HtmlResolver, E as RenderProps, S as HtmlRenderProps, T as RenderFunction, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, c as InferResponseFields, d as OpenAPIResponseType, f as PathOfType, g as TypeAtPath, h as SchemaType, i as FieldOverrides, l as JsonObject, m as SchemaMeta, n as FieldConstraints, o as InferParameterOverrides, p as ResolveOpenAPIRef, r as FieldOverride, s as InferRequestBodyFields, t as Editability, u as OpenAPIRequestBodyType, v as resolveEditability, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-DDCD6Xnx.mjs";
1
+ import { C as HtmlResolver, E as RenderProps, S as HtmlRenderProps, T as RenderFunction, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, c as InferResponseFields, d as OpenAPIResponseType, f as PathOfType, g as TypeAtPath, h as SchemaType, i as FieldOverrides, l as JsonObject, m as SchemaMeta, n as FieldConstraints, o as InferParameterOverrides, p as ResolveOpenAPIRef, r as FieldOverride, s as InferRequestBodyFields, t as Editability, u as OpenAPIRequestBodyType, v as resolveEditability, x as HtmlRenderFunction, y as BaseFieldProps } from "../types-BJzEgJdX.mjs";
2
2
  import { i as SchemaRenderError, n as SchemaFieldError, r as SchemaNormalisationError, t as SchemaError } from "../errors-DIKI2C78.mjs";
3
3
  export { BaseFieldProps, ComponentResolver, Editability, FieldConstraints, FieldOverride, FieldOverrides, FromJSONSchema, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, InferParameterOverrides, InferRequestBodyFields, InferResponseFields, JsonObject, OpenAPIRequestBodyType, OpenAPIResponseType, PathOfType, RenderFunction, RenderProps, ResolveOpenAPIRef, SchemaError, SchemaFieldError, SchemaMeta, SchemaNormalisationError, SchemaRenderError, SchemaType, TypeAtPath, WalkedField, resolveEditability };
@@ -1,4 +1,4 @@
1
- import { _ as WalkedField, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { _ as WalkedField, m as SchemaMeta } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/core/walker.d.ts
4
4
  interface WalkOptions {
@@ -160,7 +160,9 @@ const OVERRIDE_META_KEYS = new Set([
160
160
  "description",
161
161
  "title",
162
162
  "deprecated",
163
- "component"
163
+ "component",
164
+ "visible",
165
+ "order"
164
166
  ]);
165
167
  function extractSchemaMetaFields(overrides) {
166
168
  if (overrides === void 0) return void 0;
@@ -1,4 +1,4 @@
1
- import { _ as WalkedField, n as FieldConstraints } from "../types-DDCD6Xnx.mjs";
1
+ import { _ as WalkedField, n as FieldConstraints } from "../types-BJzEgJdX.mjs";
2
2
  import { HtmlAttributes, HtmlNode } from "./html.mjs";
3
3
 
4
4
  //#region src/html/a11y.d.ts
@@ -1,4 +1,4 @@
1
- import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-DDCD6Xnx.mjs";
1
+ import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/html/renderToHtml.d.ts
4
4
  interface RenderToHtmlOptions {
@@ -180,10 +180,13 @@ function renderObjectNode(props) {
180
180
  const obj = isRecord(props.value) ? props.value : {};
181
181
  const descriptionText = typeof props.meta.description === "string" ? props.meta.description : void 0;
182
182
  const legend = descriptionText !== void 0 ? h("legend", {}, descriptionText) : void 0;
183
+ const sortedEntries = Object.entries(fields).sort((a, b) => {
184
+ return (typeof a[1].meta.order === "number" ? a[1].meta.order : Infinity) - (typeof b[1].meta.order === "number" ? b[1].meta.order : Infinity);
185
+ }).filter(([, field]) => field.meta.visible !== false);
183
186
  if (props.readOnly) {
184
187
  const children = [];
185
188
  if (legend !== void 0) children.push(legend);
186
- for (const [key, field] of Object.entries(fields)) {
189
+ for (const [key, field] of sortedEntries) {
187
190
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
188
191
  const childValue = obj[key];
189
192
  const childHtml = props.renderChild(field, childValue, props.path ? `${props.path}.${key}` : key);
@@ -196,7 +199,7 @@ function renderObjectNode(props) {
196
199
  }
197
200
  const children = [];
198
201
  if (legend !== void 0) children.push(legend);
199
- for (const [key, field] of Object.entries(fields)) {
202
+ for (const [key, field] of sortedEntries) {
200
203
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
201
204
  const fieldId = buildInputId(props.path, key);
202
205
  const childPath = props.path ? `${props.path}.${key}` : key;
@@ -413,6 +416,7 @@ function renderToHtml(schema, options = {}) {
413
416
  return renderFieldHtml(tree, options.value ?? tree.defaultValue, resolver, "", renderChild);
414
417
  }
415
418
  function renderFieldHtml(tree, value, resolver, path, renderChild) {
419
+ if (tree.meta.visible === false) return "";
416
420
  const effectiveValue = value ?? tree.defaultValue;
417
421
  const mergedResolver = mergeHtmlResolvers(resolver, defaultHtmlResolver);
418
422
  const renderFn = getHtmlRenderFn(tree.type, mergedResolver);
@@ -1,4 +1,4 @@
1
- import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-DDCD6Xnx.mjs";
1
+ import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-BJzEgJdX.mjs";
2
2
  import { WidgetMap } from "../react/SchemaComponent.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
@@ -1,4 +1,4 @@
1
- import { l as JsonObject } from "../types-DDCD6Xnx.mjs";
1
+ import { l as JsonObject } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/openapi/parser.d.ts
4
4
  interface OpenApiDocument {
@@ -1,4 +1,4 @@
1
- import { E as RenderProps, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, f as PathOfType, i as FieldOverrides, m as SchemaMeta, p as ResolveOpenAPIRef, r as FieldOverride } from "../types-DDCD6Xnx.mjs";
1
+ import { E as RenderProps, _ as WalkedField, a as FromJSONSchema, b as ComponentResolver, f as PathOfType, i as FieldOverrides, m as SchemaMeta, p as ResolveOpenAPIRef, r as FieldOverride } from "../types-BJzEgJdX.mjs";
2
2
  import { t as SchemaError } from "../errors-DIKI2C78.mjs";
3
3
  import { z } from "zod";
4
4
  import { ReactNode } from "react";
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { isObject, toRecord } from "../core/guards.mjs";
2
+ import { isObject, toRecord, toRecordOrUndefined } from "../core/guards.mjs";
3
3
  import { normaliseSchema } from "../core/adapter.mjs";
4
4
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
5
5
  import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
@@ -79,14 +79,21 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
79
79
  throw error;
80
80
  }
81
81
  const handleChange = useCallback((nextValue) => {
82
- if (validate) runValidation(zodSchema, jsonSchema, nextValue, onValidationError);
82
+ if (validate) {
83
+ const error = runValidation(zodSchema, jsonSchema, nextValue);
84
+ if (error !== void 0) {
85
+ onValidationError?.(error);
86
+ dispatchFieldErrors(fields, error);
87
+ }
88
+ }
83
89
  onChange?.(nextValue);
84
90
  }, [
85
91
  validate,
86
92
  zodSchema,
87
93
  jsonSchema,
88
94
  onChange,
89
- onValidationError
95
+ onValidationError,
96
+ fields
90
97
  ]);
91
98
  const tree = walk(jsonSchema, {
92
99
  componentMeta: mergedMeta,
@@ -99,15 +106,12 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
99
106
  };
100
107
  return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets);
101
108
  }
102
- function runValidation(zodSchema, jsonSchema, value, onError) {
109
+ function runValidation(zodSchema, jsonSchema, value) {
103
110
  if (zodSchema !== void 0 && isObject(zodSchema)) {
104
111
  const safeParseFn = zodSchema.safeParse;
105
112
  if (isCallable(safeParseFn)) {
106
113
  const result = safeParseFn(value);
107
- if (isObject(result) && "success" in result && result.success !== true) {
108
- onError?.(result.error);
109
- return;
110
- }
114
+ if (isObject(result) && "success" in result && result.success !== true) return result.error;
111
115
  return;
112
116
  }
113
117
  }
@@ -116,11 +120,12 @@ function runValidation(zodSchema, jsonSchema, value, onError) {
116
120
  const safeParseFn = parsed.safeParse;
117
121
  if (isCallable(safeParseFn)) {
118
122
  const result = safeParseFn(value);
119
- if (isObject(result) && "success" in result && result.success !== true) onError?.(result.error);
123
+ if (isObject(result) && "success" in result && result.success !== true) return result.error;
120
124
  }
121
125
  }
122
126
  }
123
127
  function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets) {
128
+ if (tree.meta.visible === false) return null;
124
129
  const componentHint = tree.meta.component;
125
130
  if (typeof componentHint === "string") {
126
131
  const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
@@ -197,7 +202,8 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
197
202
  const handleChange = useCallback((nextFieldValue) => {
198
203
  if (validate) {
199
204
  const newRootValue = setNestedValue(value, path, nextFieldValue);
200
- runValidation(zodSchema, jsonSchema, newRootValue, onValidationError);
205
+ const error = runValidation(zodSchema, jsonSchema, newRootValue);
206
+ if (error !== void 0) onValidationError?.(error);
201
207
  }
202
208
  const newRootValue = setNestedValue(value, path, nextFieldValue);
203
209
  onChange?.(newRootValue);
@@ -280,6 +286,36 @@ function setNestedValue(root, path, leafValue) {
280
286
  }
281
287
  return result;
282
288
  }
289
+ function isFieldErrorCallback(value) {
290
+ return typeof value === "function";
291
+ }
292
+ /**
293
+ * Dispatch Zod errors to per-field onValidationError callbacks.
294
+ * Walks the fields override tree and matches errors by path prefix.
295
+ */
296
+ function dispatchFieldErrors(fields, error) {
297
+ if (fields === void 0 || !isObject(error)) return;
298
+ if (!("issues" in error)) return;
299
+ const issues = error.issues;
300
+ if (!Array.isArray(issues)) return;
301
+ const overrides = toRecordOrUndefined(fields);
302
+ if (overrides === void 0) return;
303
+ for (const [key, override] of Object.entries(overrides)) {
304
+ if (override === void 0 || typeof override !== "object") continue;
305
+ if (override === null) continue;
306
+ if (!("onValidationError" in override)) continue;
307
+ const fieldCallback = override.onValidationError;
308
+ if (typeof fieldCallback !== "function") continue;
309
+ const fieldErrors = issues.filter((issue) => {
310
+ if (!isObject(issue)) return false;
311
+ if (!("path" in issue)) return false;
312
+ const path = issue.path;
313
+ if (!Array.isArray(path)) return false;
314
+ return path[0] === key;
315
+ });
316
+ if (fieldErrors.length > 0 && isFieldErrorCallback(fieldCallback)) fieldCallback({ issues: fieldErrors });
317
+ }
318
+ }
283
319
  function isCallable(value) {
284
320
  return typeof value === "function";
285
321
  }
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver, m as SchemaMeta } from "../types-BJzEgJdX.mjs";
2
2
  import { WidgetMap } from "./SchemaComponent.mjs";
3
3
  import { ReactNode } from "react";
4
4
 
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-BJzEgJdX.mjs";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/react/headless.d.ts
@@ -237,7 +237,10 @@ function renderObject(props) {
237
237
  const obj = isObject(props.value) ? props.value : {};
238
238
  const fields = props.fields;
239
239
  if (fields === void 0) return null;
240
- return /* @__PURE__ */ jsxs("fieldset", { children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx("legend", { children: props.meta.description }), Object.entries(fields).map(([key, field]) => {
240
+ const sortedEntries = Object.entries(fields).sort((a, b) => {
241
+ return (typeof a[1].meta.order === "number" ? a[1].meta.order : Infinity) - (typeof b[1].meta.order === "number" ? b[1].meta.order : Infinity);
242
+ });
243
+ 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]) => {
241
244
  const childValue = obj[key];
242
245
  const childId = inputId(props.path ? `${props.path}.${key}` : key);
243
246
  const childOnChange = (v) => {
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/themes/mui.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
1
+ import { b as ComponentResolver } from "../types-BJzEgJdX.mjs";
2
2
 
3
3
  //#region src/themes/shadcn.d.ts
4
4
  declare const shadcnResolver: ComponentResolver;
@@ -147,6 +147,8 @@ interface SchemaMeta {
147
147
  deprecated?: boolean;
148
148
  /** Component hint — resolved before theme adapter. */
149
149
  component?: string;
150
+ /** Sort order for object fields. Lower values render first. */
151
+ order?: number;
150
152
  /** Arbitrary UI hints passed through to theme adapters. */
151
153
  [key: string]: unknown;
152
154
  }
@@ -166,14 +168,19 @@ type Editability = "presentation" | "input" | "editable";
166
168
  declare function resolveEditability(propertyMeta: SchemaMeta | undefined, componentMeta: SchemaMeta | undefined, rootMeta: SchemaMeta | undefined): Editability;
167
169
  /**
168
170
  * Recursive mapped type that mirrors a schema's shape for per-field
169
- * meta overrides. Each leaf is `Partial<SchemaMeta>`, objects recurse
170
- * and also accept their own `SchemaMeta`.
171
+ * overrides. Each leaf accepts schema meta overrides and an optional
172
+ * per-field validation error callback. Objects recurse and also accept
173
+ * their own overrides.
171
174
  */
172
- type FieldOverrides<T> = { [K in keyof T]?: T[K] extends object ? FieldOverrides<T[K]> & Partial<SchemaMeta> : Partial<SchemaMeta> };
175
+ type FieldOverrides<T> = { [K in keyof T]?: T[K] extends object ? FieldOverrides<T[K]> & FieldOverride : FieldOverride };
173
176
  /**
174
- * Fallback type for runtime schemas (no compile-time shape).
177
+ * Per-field override. Extends SchemaMeta with rendering controls
178
+ * and a per-field validation error callback.
175
179
  */
176
- type FieldOverride = Partial<SchemaMeta>;
180
+ type FieldOverride = Partial<SchemaMeta> & {
181
+ /** Called with the ZodError when this field fails validation. */onValidationError?: (error: unknown) => void; /** Hide this field when false. Defaults to true (visible). */
182
+ visible?: boolean;
183
+ };
177
184
  type SchemaType = "string" | "number" | "boolean" | "null" | "enum" | "literal" | "object" | "array" | "record" | "union" | "discriminatedUnion" | "optional" | "nullable" | "default" | "readonly" | "pipe" | "lazy" | "file" | "unknown";
178
185
  interface WalkedField {
179
186
  type: SchemaType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.5.1",
3
+ "version": "1.7.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",