schema-components 1.12.6 → 1.12.7

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/README.md ADDED
@@ -0,0 +1,666 @@
1
+ # schema-components
2
+
3
+ [![npm version](https://img.shields.io/npm/v/schema-components.svg)](https://www.npmjs.com/package/schema-components)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Mearman/schema-components/ci.yml?branch=main)](https://github.com/Mearman/schema-components/actions)
6
+
7
+ React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install schema-components
13
+ ```
14
+
15
+ Peer dependencies: `zod@^4.0.0`, `react@^18.0.0 || ^19.0.0`.
16
+
17
+ ## `SchemaComponent`
18
+
19
+ The single entry point. Accepts Zod schemas, JSON Schema objects, or OpenAPI documents:
20
+
21
+ ```tsx
22
+ import { SchemaComponent } from "schema-components/react/SchemaComponent";
23
+
24
+ // Zod schema
25
+ <SchemaComponent schema={z.object({ name: z.string() })} value={data} />
26
+
27
+ // JSON Schema object
28
+ <SchemaComponent schema={{ type: "object", properties: { name: { type: "string" } } }} value={data} />
29
+
30
+ // OpenAPI document + ref
31
+ <SchemaComponent schema={openApiSpec} ref="#/components/schemas/User" value={data} />
32
+ ```
33
+
34
+ ### Props
35
+
36
+ | Prop | Type | Description |
37
+ |---|---|---|
38
+ | `schema` | `ZodType \| JSONObject \| OpenAPIDocument` | The schema to render |
39
+ | `value` | `unknown` | Current value for the schema |
40
+ | `onChange` | `(value: unknown) => void` | Callback when value changes |
41
+ | `readOnly` | `boolean` | Force read-only presentation |
42
+ | `writeOnly` | `boolean` | Force write-only (blank inputs) |
43
+ | `ref` | `string` | JSON Pointer into OpenAPI document |
44
+ | `fields` | `InferFields<T>` | Type-safe per-field overrides |
45
+ | `widgets` | `WidgetMap` | Instance-scoped widget overrides |
46
+ | `validate` | `boolean` | Enable Zod validation on change |
47
+ | `onValidationError` | `(error: unknown) => void` | Callback for validation errors |
48
+ | `onError` | `(error: SchemaError) => ReactNode \| void` | Per-component error handler |
49
+ | `resolver` | `ComponentResolver` | Theme adapter override |
50
+ | `meta` | `SchemaMeta` | Schema-level metadata override |
51
+
52
+ ## Component editability
53
+
54
+ Fields render in one of three states, controlled by `readOnly` and `writeOnly` from three sources:
55
+
56
+ | State | Rendering |
57
+ |---|---|
58
+ | **Presentation** | Read-only display. Formatted text, links, badges. No inputs. |
59
+ | **Input** | Empty field. Blank inputs, "Select…" dropdowns, unchecked toggles. |
60
+ | **Editable** | Pre-populated input the user can change. |
61
+
62
+ ### Three sources, priority order
63
+
64
+ 1. **Schema property** (`.meta({ readOnly: true })`) — always wins
65
+ 2. **Component props** (`readOnly` / `writeOnly` on `<SchemaComponent>`) — rendering context
66
+ 3. **Schema root** (`.meta({ readOnly: true })` on root schema) — fallback default
67
+ 4. Neither → Editable
68
+
69
+ ### Overriding with `readOnly: false`
70
+
71
+ A field override can explicitly opt out of a higher-level `readOnly`:
72
+
73
+ ```tsx
74
+ <SchemaComponent
75
+ schema={userSchema}
76
+ value={user}
77
+ readOnly // everything presentation
78
+ fields={{
79
+ address: {
80
+ readOnly: false, // address subtree: editable
81
+ city: { readOnly: true }, // city: still presentation
82
+ },
83
+ }}
84
+ />
85
+ ```
86
+
87
+ ## Type-safe field overrides
88
+
89
+ The `fields` prop type is inferred from the schema:
90
+
91
+ ```tsx
92
+ // Zod — full autocomplete
93
+ <SchemaComponent
94
+ schema={userSchema}
95
+ fields={{
96
+ name: { readOnly: true }, // ✓ type-safe
97
+ address: {
98
+ city: { description: "City" }, // ✓ nested, type-safe
99
+ },
100
+ // nme: { readOnly: true }, // ✗ TypeScript error: unknown key
101
+ }}
102
+ />
103
+
104
+ // JSON Schema as const — full autocomplete
105
+ const jsonSchema = {
106
+ type: "object" as const,
107
+ properties: {
108
+ name: { type: "string" as const },
109
+ email: { type: "string" as const, format: "email" },
110
+ },
111
+ required: ["name"],
112
+ } as const;
113
+
114
+ <SchemaComponent
115
+ schema={jsonSchema}
116
+ fields={{
117
+ name: { readOnly: true }, // ✓ inferred from as const
118
+ // nme: { readOnly: true }, // ✗ TypeScript error
119
+ }}
120
+ />
121
+
122
+ // OpenAPI as const + ref — full autocomplete
123
+ const spec = {
124
+ openapi: "3.1.0",
125
+ components: {
126
+ schemas: {
127
+ User: {
128
+ type: "object" as const,
129
+ properties: {
130
+ id: { type: "string" as const },
131
+ name: { type: "string" as const },
132
+ },
133
+ required: ["id", "name"],
134
+ },
135
+ },
136
+ },
137
+ } as const;
138
+
139
+ <SchemaComponent
140
+ schema={spec}
141
+ ref="#/components/schemas/User"
142
+ fields={{
143
+ id: { readOnly: true }, // ✓ inferred through ref
144
+ }}
145
+ />
146
+ ```
147
+
148
+ ### `FieldOverride`
149
+
150
+ Each field override accepts:
151
+
152
+ | Property | Type | Description |
153
+ |---|---|---|
154
+ | `readOnly` | `boolean` | Override editability for this field |
155
+ | `writeOnly` | `boolean` | Override write-only state |
156
+ | `visible` | `boolean` | Hide the field entirely when `false` |
157
+ | `order` | `number` | Sort order within parent object |
158
+ | `onValidationError` | `(error: unknown) => void` | Per-field validation callback |
159
+ | `description` | `string` | Override label / description |
160
+ | `default` | `unknown` | Override default value |
161
+ | `component` | `string` | Widget name for custom rendering |
162
+
163
+ Plus any standard JSON Schema meta properties (`title`, `format`, `pattern`, etc.).
164
+
165
+ ## Individual fields
166
+
167
+ ```tsx
168
+ import { SchemaField } from "schema-components/react/SchemaComponent";
169
+
170
+ // Type-safe path — only valid dot-paths accepted
171
+ <SchemaField
172
+ schema={userSchema}
173
+ path="address.city" // ✓ type-safe
174
+ // path="address.cty" // ✗ TypeScript error
175
+ value={user}
176
+ onChange={setUser}
177
+ />
178
+ ```
179
+
180
+ When the schema is a Zod schema or typed `as const`, only valid dot-paths like `"address.city"` are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.
181
+
182
+ ## OpenAPI components
183
+
184
+ Render API operations with type-safe field overrides:
185
+
186
+ ```tsx
187
+ import {
188
+ ApiOperation,
189
+ ApiParameters,
190
+ ApiRequestBody,
191
+ ApiResponse,
192
+ } from "schema-components/openapi/components";
193
+
194
+ const petStore = {
195
+ openapi: "3.1.0",
196
+ paths: {
197
+ "/pets": {
198
+ post: {
199
+ requestBody: {
200
+ content: {
201
+ "application/json": {
202
+ schema: {
203
+ type: "object" as const,
204
+ properties: {
205
+ name: { type: "string" as const },
206
+ tag: { type: "string" as const },
207
+ },
208
+ required: ["name"],
209
+ },
210
+ },
211
+ },
212
+ },
213
+ responses: { "201": { description: "Created" } },
214
+ },
215
+ },
216
+ },
217
+ } as const;
218
+
219
+ // Full operation — parameters, request body, responses
220
+ <ApiOperation schema={petStore} path="/pets" method="post" />
221
+
222
+ // Just the request body with type-safe fields
223
+ <ApiRequestBody
224
+ schema={petStore}
225
+ path="/pets"
226
+ method="post"
227
+ fields={{
228
+ name: { description: "Pet name" }, // ✓ inferred from as const
229
+ }}
230
+ />
231
+
232
+ // Just parameters with type-safe overrides
233
+ <ApiParameters
234
+ schema={petStore}
235
+ path="/pets"
236
+ method="get"
237
+ overrides={{
238
+ limit: { description: "Max results" },
239
+ }}
240
+ />
241
+
242
+ // Response schema
243
+ <ApiResponse schema={petStore} path="/pets" method="get" status="200" />
244
+ ```
245
+
246
+ ## Theme adapters
247
+
248
+ Headless by default (plain HTML). Wrap with a theme adapter for styled components:
249
+
250
+ ### shadcn/ui
251
+
252
+ ```tsx
253
+ import { SchemaProvider } from "schema-components/react/SchemaComponent";
254
+ import { shadcnResolver } from "schema-components/themes/shadcn";
255
+
256
+ <SchemaProvider resolver={shadcnResolver}>
257
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
258
+ </SchemaProvider>
259
+ ```
260
+
261
+ ### MUI
262
+
263
+ ```tsx
264
+ import { registerMuiComponents } from "schema-components/themes/mui";
265
+ import { shadcnResolver } from "schema-components/themes/shadcn";
266
+
267
+ // Register MUI components at app startup
268
+ registerMuiComponents();
269
+
270
+ // Use via SchemaProvider
271
+ <SchemaProvider resolver={shadcnResolver}>
272
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
273
+ </SchemaProvider>
274
+ ```
275
+
276
+ ### Mantine
277
+
278
+ ```tsx
279
+ import { registerMantineComponents } from "schema-components/themes/mantine";
280
+ import { shadcnResolver } from "schema-components/themes/shadcn";
281
+
282
+ registerMantineComponents();
283
+
284
+ <SchemaProvider resolver={shadcnResolver}>
285
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
286
+ </SchemaProvider>
287
+ ```
288
+
289
+ ### Radix Themes
290
+
291
+ ```tsx
292
+ import { registerRadixComponents } from "schema-components/themes/radix";
293
+ import { shadcnResolver } from "schema-components/themes/shadcn";
294
+
295
+ registerRadixComponents();
296
+
297
+ <SchemaProvider resolver={shadcnResolver}>
298
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
299
+ </SchemaProvider>
300
+ ```
301
+
302
+ ### Custom adapter
303
+
304
+ ```tsx
305
+ import type { RenderProps, ComponentResolver } from "schema-components/core/renderer";
306
+
307
+ const myResolver: ComponentResolver = {
308
+ string: (props: RenderProps) => {
309
+ if (props.readOnly) return <span>{props.value}</span>;
310
+ return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
311
+ },
312
+ object: (props: RenderProps) => {
313
+ return (
314
+ <div>
315
+ {props.fields && Object.entries(props.fields).map(([key, field]) => (
316
+ <div key={key}>
317
+ <label>{field.meta.description}</label>
318
+ {props.renderChild(field, (props.value as Record<string, unknown>)?.[key], (v) => {
319
+ props.onChange({ ...(props.value as object), [key]: v });
320
+ })}
321
+ </div>
322
+ ))}
323
+ </div>
324
+ );
325
+ },
326
+ };
327
+ ```
328
+
329
+ Every render function receives `props.renderChild` for recursive rendering — no need to know about the resolver or rendering context.
330
+
331
+ ## Custom widgets
332
+
333
+ Widgets let you override rendering for specific fields using `.meta({ component: name })`. Three scopes are available, checked in order:
334
+
335
+ 1. **Instance** — `widgets` prop on `<SchemaComponent>`
336
+ 2. **Context** — `widgets` prop on `<SchemaProvider>`
337
+ 3. **Global** — `registerWidget()` for app-wide defaults
338
+
339
+ ### Global registration
340
+
341
+ ```tsx
342
+ import { registerWidget } from "schema-components/react/SchemaComponent";
343
+
344
+ registerWidget("richtext", ({ value, onChange }) => (
345
+ <RichTextEditor value={value} onChange={onChange} />
346
+ ));
347
+
348
+ const schema = z.object({
349
+ bio: z.string().meta({ component: "richtext" }),
350
+ });
351
+ ```
352
+
353
+ ### Context-scoped widgets
354
+
355
+ ```tsx
356
+ import { SchemaProvider } from "schema-components/react/SchemaComponent";
357
+ import type { WidgetMap } from "schema-components/react/SchemaComponent";
358
+
359
+ const adminWidgets: WidgetMap = new Map([
360
+ ["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
361
+ ["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
362
+ ]);
363
+
364
+ <SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
365
+ <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
366
+ <SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
367
+ </SchemaProvider>
368
+ ```
369
+
370
+ ### Instance-scoped widgets
371
+
372
+ ```tsx
373
+ const formWidgets: WidgetMap = new Map([
374
+ ["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
375
+ ]);
376
+
377
+ <SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />
378
+ ```
379
+
380
+ ### Resolution order
381
+
382
+ ```
383
+ .meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default
384
+ ```
385
+
386
+ ### `WidgetMap` type
387
+
388
+ ```tsx
389
+ import type { WidgetMap } from "schema-components/react/SchemaComponent";
390
+
391
+ // ReadonlyMap<string, (props: RenderProps) => unknown>
392
+ const widgets: WidgetMap = new Map([
393
+ ["name", (props) => <MyInput {...props} />],
394
+ ]);
395
+ ```
396
+
397
+ Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React context available):
398
+
399
+ ```tsx
400
+ <SchemaView schema={schema} value={data} widgets={serverWidgets} />
401
+ ```
402
+
403
+ ## Validation
404
+
405
+ ```tsx
406
+ <SchemaComponent
407
+ schema={userSchema}
408
+ value={user}
409
+ onChange={setUser}
410
+ validate
411
+ onValidationError={(error) => console.error(error)}
412
+ />
413
+ ```
414
+
415
+ Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
416
+
417
+ ### Per-field validation errors
418
+
419
+ Add `onValidationError` to individual field overrides to receive errors for specific fields:
420
+
421
+ ```tsx
422
+ <SchemaComponent
423
+ schema={userSchema}
424
+ value={user}
425
+ onChange={setUser}
426
+ validate
427
+ fields={{
428
+ email: { onValidationError: (err) => setEmailError(err) },
429
+ name: { onValidationError: (err) => setNameError(err) },
430
+ }}
431
+ />
432
+ ```
433
+
434
+ Errors are dispatched based on Zod error paths. The root-level `onValidationError` still receives all errors.
435
+
436
+ ## Field visibility
437
+
438
+ Hide fields conditionally using the `visible` override:
439
+
440
+ ```tsx
441
+ <SchemaComponent
442
+ schema={paymentSchema}
443
+ value={payment}
444
+ fields={{
445
+ cardNumber: { visible: payment.method === "card" },
446
+ sortCode: { visible: payment.method === "bank" },
447
+ }}
448
+ />
449
+ ```
450
+
451
+ When `visible: false`, the field is completely removed — no label, no empty placeholder, no hidden input.
452
+
453
+ ## Field ordering
454
+
455
+ Control the order fields appear in rendered objects using `order`:
456
+
457
+ ```tsx
458
+ <SchemaComponent
459
+ schema={userSchema}
460
+ value={user}
461
+ fields={{
462
+ email: { order: 1 },
463
+ name: { order: 2 },
464
+ role: { order: 3 },
465
+ }}
466
+ />
467
+ ```
468
+
469
+ Lower `order` values render first. Fields without `order` keep their insertion order and appear after ordered fields. Can also be set in schema metadata:
470
+
471
+ ```tsx
472
+ const schema = z.object({
473
+ summary: z.string().meta({ order: 1 }),
474
+ title: z.string().meta({ order: 2 }),
475
+ });
476
+ ```
477
+
478
+ ## Discriminated unions
479
+
480
+ 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.
481
+
482
+ ```tsx
483
+ const payment = z.discriminatedUnion("method", [
484
+ z.object({
485
+ method: z.literal("card"),
486
+ cardNumber: z.string(),
487
+ expiry: z.string(),
488
+ }),
489
+ z.object({
490
+ method: z.literal("bank"),
491
+ accountNumber: z.string(),
492
+ sortCode: z.string(),
493
+ }),
494
+ ]);
495
+
496
+ <SchemaComponent schema={payment} value={{ method: "card", cardNumber: "4111...", expiry: "12/28" }} />
497
+ ```
498
+
499
+ In read-only mode, only the active variant is rendered (no tabs).
500
+
501
+ ## Date and time inputs
502
+
503
+ String schemas with `format: "date"`, `format: "time"`, or `format: "date-time"` render as the corresponding HTML5 input types:
504
+
505
+ ```tsx
506
+ const eventSchema = z.object({
507
+ date: z.string().meta({ format: "date" }),
508
+ startTime: z.string().meta({ format: "time" }),
509
+ createdAt: z.string().meta({ format: "date-time" }),
510
+ });
511
+ ```
512
+
513
+ This produces `<input type="date">`, `<input type="time">`, and `<input type="datetime-local">` respectively. In read-only mode, dates are formatted using `toLocaleDateString()` / `toLocaleString()`.
514
+
515
+ ## Schema defaults
516
+
517
+ Default values from `z.string().default("hello")` or JSON Schema `"default": "hello"` are used when the `value` prop is `undefined`:
518
+
519
+ ```tsx
520
+ const schema = z.object({
521
+ name: z.string().default("World"),
522
+ count: z.number().default(0),
523
+ });
524
+
525
+ // Renders with "World" and 0 pre-filled
526
+ <SchemaComponent schema={schema} />
527
+ ```
528
+
529
+ Defaults propagate through nested objects — each field uses its own default independently.
530
+
531
+ ## File uploads
532
+
533
+ String schemas with `format: "binary"` render as `<input type="file">`. Use `contentMediaType` to restrict accepted MIME types:
534
+
535
+ ```tsx
536
+ const schema = z.object({
537
+ avatar: z.string().meta({ format: "binary" }),
538
+ resume: z.string().meta({ format: "binary", contentMediaType: "application/pdf" }),
539
+ });
540
+ ```
541
+
542
+ In read-only mode, file fields display a static label ("File field") since there is no value to show. The `onChange` callback receives the `File` object from the browser.
543
+
544
+ ## Server Components
545
+
546
+ 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.
547
+
548
+ ```tsx
549
+ import { SchemaView } from "schema-components/react/SchemaView";
550
+
551
+ export default async function Page() {
552
+ const user = await getUser();
553
+ return <SchemaView schema={userSchema} value={user} />;
554
+ }
555
+ ```
556
+
557
+ `SchemaView` always renders read-only. For editable forms, use `<SchemaComponent>` (which requires `"use client"`).
558
+
559
+ Pass the resolver explicitly since React context is unavailable in Server Components:
560
+
561
+ ```tsx
562
+ <SchemaView schema={schema} value={data} resolver={shadcnResolver} />
563
+ ```
564
+
565
+ `SchemaView` produces identical output to `<SchemaComponent readOnly>` — verified by parity tests.
566
+
567
+ ## HTML rendering
568
+
569
+ Render schemas to HTML strings — no React needed. Useful for server-side rendering, email templates, static sites, and non-React environments.
570
+
571
+ ```tsx
572
+ import { renderToHtml } from "schema-components/html/renderToHtml";
573
+
574
+ const html = renderToHtml(userSchema, {
575
+ value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
576
+ readOnly: true,
577
+ });
578
+ ```
579
+
580
+ All HTML output uses `sc-` prefixed classes for styling hooks. HTML is properly escaped by the serialiser.
581
+
582
+ A default stylesheet is included:
583
+
584
+ ```html
585
+ <link rel="stylesheet" href="node_modules/schema-components/dist/html/styles.css">
586
+ ```
587
+
588
+ Or import in JS:
589
+
590
+ ```ts
591
+ import "schema-components/styles.css";
592
+ ```
593
+
594
+ ### Streaming HTML
595
+
596
+ Three output formats for incremental rendering:
597
+
598
+ ```ts
599
+ import { renderToHtmlChunks } from "schema-components/html/renderToHtmlStream";
600
+ import { renderToHtmlStream } from "schema-components/html/renderToHtmlStream";
601
+ import { renderToHtmlReadable } from "schema-components/html/renderToHtmlStream";
602
+
603
+ // Sync iterable — chunks yielded at field/item/entry boundaries
604
+ const chunks: string[] = [...renderToHtmlChunks(schema, { value })];
605
+
606
+ // Async iterable — yields control to event loop between chunks
607
+ for await (const chunk of renderToHtmlStream(schema, { value })) {
608
+ res.write(chunk);
609
+ }
610
+
611
+ // Web ReadableStream — for Response, TransformStream, etc.
612
+ return new Response(renderToHtmlReadable(schema, { value }), {
613
+ headers: { "Content-Type": "text/html" },
614
+ });
615
+ ```
616
+
617
+ ### Structured HTML construction
618
+
619
+ The HTML renderer uses a typed `h()` builder instead of string templates:
620
+
621
+ ```ts
622
+ import { h, serialize, raw } from "schema-components/html/html";
623
+
624
+ const input = h("input", { type: "text", id: "name", value: userValue });
625
+ serialize(input); // → <input type="text" id="name" value="Ada">
626
+ ```
627
+
628
+ The builder handles void elements, boolean attributes, fragments, and nested children.
629
+
630
+ ### Accessibility
631
+
632
+ The HTML renderer produces WAI-ARIA-compliant markup:
633
+
634
+ | Attribute | When |
635
+ |---|---|
636
+ | `id="<key>"` | All editable inputs |
637
+ | `aria-required="true"` | Required fields |
638
+ | `aria-describedby="<id>-hint"` | Fields with constraints |
639
+ | `aria-readonly="true"` | Read-only presentation spans |
640
+ | `aria-label="<description>"` | Checkboxes |
641
+ | `role="group"` | Record containers |
642
+
643
+ ## Error handling
644
+
645
+ Typed errors with `onError` callback for graceful degradation:
646
+
647
+ ```tsx
648
+ import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";
649
+
650
+ // Error boundary catches render errors from theme adapters
651
+ <SchemaErrorBoundary fallback={(error, reset) => <p>Error: {error.message}</p>}>
652
+ <SchemaComponent schema={schema} value={data} />
653
+ </SchemaErrorBoundary>
654
+
655
+ // Per-component error callback
656
+ <SchemaComponent
657
+ schema={schema}
658
+ value={data}
659
+ onError={(error) => {
660
+ console.error(error);
661
+ return null; // graceful degradation
662
+ }}
663
+ />
664
+ ```
665
+
666
+ Without `onError`, errors re-throw. Error hierarchy: `SchemaError` → `SchemaNormalisationError` | `SchemaRenderError` | `SchemaFieldError`.
@@ -292,7 +292,7 @@ function renderArray(props) {
292
292
  const arr = Array.isArray(props.value) ? props.value : [];
293
293
  const element = props.element;
294
294
  if (element === void 0) return null;
295
- if (props.readOnly && arr.length === 0) return null;
295
+ if (arr.length === 0) return null;
296
296
  return /* @__PURE__ */ jsx("div", {
297
297
  role: "group",
298
298
  "aria-label": props.meta.description ?? void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.12.6",
3
+ "version": "1.12.7",
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",