schema-components 1.6.0 → 1.7.1

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,15 @@
1
+ ## [1.7.1](https://github.com/Mearman/schema-components/compare/v1.7.0...v1.7.1) (2026-05-14)
2
+
3
+ ### Documentation
4
+
5
+ * update README and add Storybook stories for visibility, ordering, and per-field validation ([4be4b6f](https://github.com/Mearman/schema-components/commit/4be4b6feaaa4819937280c010eacb3f36a521a54))
6
+
7
+ ## [1.7.0](https://github.com/Mearman/schema-components/compare/v1.6.0...v1.7.0) (2026-05-14)
8
+
9
+ ### Features
10
+
11
+ * add field visibility and ordering controls ([27cb3e1](https://github.com/Mearman/schema-components/commit/27cb3e19ba930200116c79293078fa6fa2743723))
12
+
1
13
  ## [1.6.0](https://github.com/Mearman/schema-components/compare/v1.5.1...v1.6.0) (2026-05-14)
2
14
 
3
15
  ### Features
package/README.md CHANGED
@@ -525,6 +525,25 @@ Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React co
525
525
 
526
526
  Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
527
527
 
528
+ ### Per-field validation errors
529
+
530
+ Add `onValidationError` to individual field overrides to receive errors for specific fields:
531
+
532
+ ```tsx
533
+ <SchemaComponent
534
+ schema={userSchema}
535
+ value={user}
536
+ onChange={setUser}
537
+ validate
538
+ fields={{
539
+ email: { onValidationError: (err) => setEmailError(err) },
540
+ name: { onValidationError: (err) => setNameError(err) },
541
+ }}
542
+ />
543
+ ```
544
+
545
+ Errors are dispatched based on Zod error paths. The root-level `onValidationError` still receives all errors.
546
+
528
547
  ## Discriminated unions
529
548
 
530
549
  Discriminated unions (`z.discriminatedUnion` or JSON Schema `oneOf` with `const` properties) render as tabbed panels. Each tab is labelled by the discriminator's `const` value. Clicking a tab resets the value with the new discriminator.
@@ -578,6 +597,48 @@ const schema = z.object({
578
597
 
579
598
  Defaults propagate through nested objects — each field uses its own default independently.
580
599
 
600
+ ## Field visibility
601
+
602
+ Hide fields conditionally using the `visible` override:
603
+
604
+ ```tsx
605
+ <SchemaComponent
606
+ schema={paymentSchema}
607
+ value={payment}
608
+ fields={{
609
+ cardNumber: { visible: payment.method === "card" },
610
+ sortCode: { visible: payment.method === "bank" },
611
+ }}
612
+ />
613
+ ```
614
+
615
+ When `visible: false`, the field is completely removed — no label, no empty placeholder, no hidden input. The parent component controls visibility based on the current value.
616
+
617
+ ## Field ordering
618
+
619
+ Control the order fields appear in rendered objects using `order`:
620
+
621
+ ```tsx
622
+ <SchemaComponent
623
+ schema={userSchema}
624
+ value={user}
625
+ fields={{
626
+ email: { order: 1 },
627
+ name: { order: 2 },
628
+ role: { order: 3 },
629
+ }}
630
+ />
631
+ ```
632
+
633
+ Lower `order` values render first. Fields without `order` keep their insertion order and appear after ordered fields. Can also be set in schema metadata:
634
+
635
+ ```tsx
636
+ const schema = z.object({
637
+ summary: z.string().meta({ order: 1 }),
638
+ title: z.string().meta({ order: 2 }),
639
+ });
640
+ ```
641
+
581
642
  ## Server Components
582
643
 
583
644
  For read-only rendering in a React Server Component, use `<SchemaView>`. It has zero hooks — no `useContext`, no `useMemo`, no `useCallback` — so it works without the `"use client"` directive.
@@ -1,4 +1,4 @@
1
- import { l as JsonObject, m as SchemaMeta } from "../types-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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";
@@ -125,6 +125,7 @@ function runValidation(zodSchema, jsonSchema, value) {
125
125
  }
126
126
  }
127
127
  function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets) {
128
+ if (tree.meta.visible === false) return null;
128
129
  const componentHint = tree.meta.component;
129
130
  if (typeof componentHint === "string") {
130
131
  const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver, m as SchemaMeta } from "../types-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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-CnlV7bBK.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
  }
@@ -172,11 +174,12 @@ declare function resolveEditability(propertyMeta: SchemaMeta | undefined, compon
172
174
  */
173
175
  type FieldOverrides<T> = { [K in keyof T]?: T[K] extends object ? FieldOverrides<T[K]> & FieldOverride : FieldOverride };
174
176
  /**
175
- * Per-field override. Extends SchemaMeta with a React-layer callback
176
- * for per-field validation errors.
177
+ * Per-field override. Extends SchemaMeta with rendering controls
178
+ * and a per-field validation error callback.
177
179
  */
178
180
  type FieldOverride = Partial<SchemaMeta> & {
179
- /** Called with the ZodError when this field fails validation. */onValidationError?: (error: unknown) => void;
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;
180
183
  };
181
184
  type SchemaType = "string" | "number" | "boolean" | "null" | "enum" | "literal" | "object" | "array" | "record" | "union" | "discriminatedUnion" | "optional" | "nullable" | "default" | "readonly" | "pipe" | "lazy" | "file" | "unknown";
182
185
  interface WalkedField {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
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",