schema-components 1.0.0 → 1.2.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,42 @@
1
+ ## [1.2.0](https://github.com/Mearman/schema-components/compare/v1.1.0...v1.2.0) (2026-05-14)
2
+
3
+ ### Features
4
+
5
+ * add date/time and discriminated union stories ([595c559](https://github.com/Mearman/schema-components/commit/595c559c98beab7fdf28790513597bfce8c773cd))
6
+ * comprehensive accessibility improvements ([ed11b2e](https://github.com/Mearman/schema-components/commit/ed11b2e7c408960a94c27d65f0869836720e179c))
7
+ * discriminated union UI, date/time inputs, schema defaults ([5cde96e](https://github.com/Mearman/schema-components/commit/5cde96e77e6af642b4973cc84b0aff129813318f))
8
+ * file upload renderer ([3742384](https://github.com/Mearman/schema-components/commit/37423849d045e9e044667d1206336207b7c60f05))
9
+ * mui theme adapter and coverage enforcement ([adeb0b6](https://github.com/Mearman/schema-components/commit/adeb0b6bab3ee2c34fe78d26db10f5d082396cc6))
10
+ * ssr tests and server component (SchemaView) ([7268f09](https://github.com/Mearman/schema-components/commit/7268f0918981030b55005678168cb3726c75dd74))
11
+
12
+ ### Bug Fixes
13
+
14
+ * exclude ssr e2e test from tsconfig ([f9248cd](https://github.com/Mearman/schema-components/commit/f9248cd32c809992873153e268c77b26d4b3f1a1))
15
+
16
+ ### Refactoring
17
+
18
+ * rename ssr test from integration to e2e ([b373e3a](https://github.com/Mearman/schema-components/commit/b373e3aa8842906ce4a53259b640eaad9f410fb6))
19
+
20
+ ### Documentation
21
+
22
+ * add discriminated unions, date/time, defaults to README ([91bc96c](https://github.com/Mearman/schema-components/commit/91bc96c7700c81c8c903f2ba9e1a6ced4e4b5c83))
23
+
24
+ ### CI
25
+
26
+ * add ssr e2e step and separate test script ([e023d8b](https://github.com/Mearman/schema-components/commit/e023d8bddb2f34d88059a74cab05ef391f4957b8))
27
+
28
+ ### Chores
29
+
30
+ * downgrade storybook 10.4.0 to 10.3.6 and vite 8.0.12 to 8.0.11 ([e3f8341](https://github.com/Mearman/schema-components/commit/e3f83419cea890517d69fda8ea0a3ef3ba8fb619))
31
+ * update dependencies (tsdown 0.22.0) ([1fc7dc0](https://github.com/Mearman/schema-components/commit/1fc7dc05f91fdabd98317684eb8bce49136c3c32))
32
+
33
+ ## [1.1.0](https://github.com/Mearman/schema-components/compare/v1.0.0...v1.1.0) (2026-05-14)
34
+
35
+ ### Features
36
+
37
+ * add validation, recursive, and OpenAPI operation stories ([d1b742e](https://github.com/Mearman/schema-components/commit/d1b742e58e57e7c11ce36a7a868e1efc2574a04b))
38
+ * expand Storybook coverage with JSON Schema, streaming, and error stories ([4378701](https://github.com/Mearman/schema-components/commit/4378701b29bcd1ebec5b8caf46bdd452d4b0f766))
39
+
1
40
  ## 1.0.0 (2026-05-14)
2
41
 
3
42
  ### Features
package/README.md CHANGED
@@ -465,6 +465,98 @@ Resolution order: `.meta({ component })` → registered widget → theme adapter
465
465
 
466
466
  Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
467
467
 
468
+ ## Discriminated unions
469
+
470
+ 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.
471
+
472
+ ```tsx
473
+ const payment = z.discriminatedUnion("method", [
474
+ z.object({
475
+ method: z.literal("card"),
476
+ cardNumber: z.string(),
477
+ expiry: z.string(),
478
+ }),
479
+ z.object({
480
+ method: z.literal("bank"),
481
+ accountNumber: z.string(),
482
+ sortCode: z.string(),
483
+ }),
484
+ ]);
485
+
486
+ <SchemaComponent schema={payment} value={{ method: "card", cardNumber: "4111...", expiry: "12/28" }} />
487
+ ```
488
+
489
+ In read-only mode, only the active variant is rendered (no tabs).
490
+
491
+ ## Date and time inputs
492
+
493
+ String schemas with `format: "date"`, `format: "time"`, or `format: "date-time"` render as the corresponding HTML5 input types:
494
+
495
+ ```tsx
496
+ const eventSchema = z.object({
497
+ date: z.string().meta({ format: "date" }),
498
+ startTime: z.string().meta({ format: "time" }),
499
+ createdAt: z.string().meta({ format: "date-time" }),
500
+ });
501
+ ```
502
+
503
+ This produces `<input type="date">`, `<input type="time">`, and `<input type="datetime-local">` respectively. In read-only mode, dates are formatted using `toLocaleDateString()` / `toLocaleString()`.
504
+
505
+ ## Schema defaults
506
+
507
+ Default values from `z.string().default("hello")` or JSON Schema `"default": "hello"` are used when the `value` prop is `undefined`:
508
+
509
+ ```tsx
510
+ const schema = z.object({
511
+ name: z.string().default("World"),
512
+ count: z.number().default(0),
513
+ });
514
+
515
+ // Renders with "World" and 0 pre-filled
516
+ <SchemaComponent schema={schema} />
517
+ ```
518
+
519
+ Defaults propagate through nested objects — each field uses its own default independently.
520
+
521
+ ## Server Components
522
+
523
+ 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.
524
+
525
+ ```tsx
526
+ import { SchemaView } from "schema-components/react/SchemaView";
527
+
528
+ export default async function Page() {
529
+ const user = await getUser();
530
+ return <SchemaView schema={userSchema} value={user} />;
531
+ }
532
+ ```
533
+
534
+ `SchemaView` always renders read-only. For editable forms, use `<SchemaComponent>` (which requires `"use client"`).
535
+
536
+ Pass the resolver explicitly since React context is unavailable in Server Components:
537
+
538
+ ```tsx
539
+ import { SchemaView } from "schema-components/react/SchemaView";
540
+ import { shadcnResolver } from "schema-components/themes/shadcn";
541
+
542
+ <SchemaView schema={schema} value={data} resolver={shadcnResolver} />
543
+ ```
544
+
545
+ `SchemaView` produces identical output to `<SchemaComponent readOnly>` — verified by parity tests.
546
+
547
+ ## File uploads
548
+
549
+ String schemas with `format: "binary"` render as `<input type="file">`. Use `contentMediaType` to restrict accepted MIME types:
550
+
551
+ ```tsx
552
+ const schema = z.object({
553
+ avatar: z.string().meta({ format: "binary" }),
554
+ resume: z.string().meta({ format: "binary", contentMediaType: "application/pdf" }),
555
+ });
556
+ ```
557
+
558
+ 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.
559
+
468
560
  ## Error handling
469
561
 
470
562
  Typed errors with `onError` callback for graceful degradation:
@@ -496,31 +588,19 @@ Without `onError`, errors re-throw. Error hierarchy: `SchemaError` → `SchemaNo
496
588
  ```
497
589
  schema-components
498
590
  ├── core # JSON Schema walker, ComponentResolver, RenderProps, typed errors, type guards
499
- ├── react # SchemaComponent, SchemaProvider, SchemaField, headless renderer, error boundary
591
+ ├── react # SchemaComponent ("use client"), SchemaView (server component), headless renderer, error boundary
500
592
  ├── openapi # Document parser, ApiOperation, ApiParameters, ApiRequestBody, ApiResponse
501
593
  ├── html # h() builder, renderToHtml, streaming renderers, ARIA helpers
502
594
  └── themes # shadcn, MUI, custom adapters (separate packages)
503
595
  ```
504
596
 
505
- ## Source files
506
-
507
- Every module is imported directly — no barrel files.
597
+ Every module is imported directly — no barrel files. Organised exports:
508
598
 
509
- | File | Role |
510
- |---|---|
511
- | `core/types.ts` | SchemaMeta, Editability, WalkedField, FieldConstraints, FieldOverrides, FromJSONSchema, PathOfType |
512
- | `core/walker.ts` | JSON Schema walker (Draft 2020-12), `$ref` resolution, `allOf` merging, nullable/discriminated union detection |
513
- | `core/adapter.ts` | Normalises all inputs to JSON Schema. WeakMap schema cache. |
514
- | `core/renderer.ts` | `BaseFieldProps`, `RenderProps`, `HtmlRenderProps`, `ComponentResolver`, `HtmlResolver`, `mergeResolvers` |
515
- | `core/guards.ts` | Shared type guards: `isObject`, `getProperty`, `hasProperty`, `toRecord` |
516
- | `core/errors.ts` | `SchemaError`, `SchemaNormalisationError`, `SchemaRenderError`, `SchemaFieldError` |
517
- | `react/SchemaComponent.tsx` | Generic `<SchemaComponent<T, Ref>>`, `SchemaProvider`, `registerWidget`, `SchemaField<P>` |
518
- | `react/headless.tsx` | Headless default resolver, `createHeadlessResolver(renderChild)` factory |
519
- | `react/SchemaErrorBoundary.tsx` | React error boundary catching render errors |
520
- | `html/html.ts` | Typed `h()` builder — `serialize()`, `serializeChunks()`, `raw()`, `text()`, `fragment()` |
521
- | `html/a11y.ts` | ARIA helpers — `ariaRequiredAttrs()`, `ariaDescribedByAttrs()`, `buildHintElement()`, `requiredIndicator()` |
522
- | `html/renderToHtml.ts` | `renderToHtml()`, `defaultHtmlResolver` — schema → HTML string via `h()` builder |
523
- | `html/renderToHtmlStream.ts` | `renderToHtmlChunks()` (sync), `renderToHtmlStream()` (async), `renderToHtmlReadable()` (web ReadableStream) |
524
- | `openapi/parser.ts` | OpenAPI document parsing, operation extraction, `$ref` resolution |
525
- | `openapi/components.tsx` | `ApiOperation`, `ApiParameters`, `ApiRequestBody`, `ApiResponse` with generic type inference |
526
- | `themes/shadcn.tsx` | shadcn/ui theme adapter |
599
+ ```
600
+ schema-components/core/* # Walker, types, guards, errors, resolver
601
+ schema-components/react/* # SchemaComponent, SchemaView, SchemaErrorBoundary, headless
602
+ schema-components/openapi/* # Parser, ApiOperation, ApiParameters, etc.
603
+ schema-components/html/* # renderToHtml, renderToHtmlChunks, h() builder, styles
604
+ schema-components/themes/* # shadcn, MUI, custom adapters
605
+ schema-components/styles.css # Default stylesheet for HTML output
606
+ ```
@@ -1,4 +1,4 @@
1
- import { l as JsonObject, m as SchemaMeta } from "../types-BU0ETFHk.mjs";
1
+ import { l as JsonObject, m as SchemaMeta } from "../types-DDCD6Xnx.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-BU0ETFHk.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-DDCD6Xnx.mjs";
2
2
  export { BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
@@ -8,6 +8,7 @@ const RESOLVER_KEYS = [
8
8
  "array",
9
9
  "record",
10
10
  "union",
11
+ "discriminatedUnion",
11
12
  "literal",
12
13
  "file",
13
14
  "unknown"
@@ -26,10 +27,10 @@ function typeToKey(type) {
26
27
  case "array":
27
28
  case "record":
28
29
  case "union":
30
+ case "discriminatedUnion":
29
31
  case "literal":
30
32
  case "file":
31
33
  case "unknown": return type;
32
- case "discriminatedUnion": return "union";
33
34
  default: return "unknown";
34
35
  }
35
36
  }
@@ -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-BU0ETFHk.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-DDCD6Xnx.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-BU0ETFHk.mjs";
1
+ import { _ as WalkedField, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
2
2
 
3
3
  //#region src/core/walker.d.ts
4
4
  interface WalkOptions {
@@ -1,4 +1,4 @@
1
- import { _ as WalkedField, n as FieldConstraints } from "../types-BU0ETFHk.mjs";
1
+ import { _ as WalkedField, n as FieldConstraints } from "../types-DDCD6Xnx.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-BU0ETFHk.mjs";
1
+ import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-DDCD6Xnx.mjs";
2
2
 
3
3
  //#region src/html/renderToHtml.d.ts
4
4
  interface RenderToHtmlOptions {
@@ -24,6 +24,18 @@ import { ariaDescribedByAttrs, ariaLabelAttrs, ariaReadonlyAttrs, ariaRequiredAt
24
24
  * resolver: { string: (props) => h("b", {}, String(props.value)) },
25
25
  * });
26
26
  */
27
+ function dateInputType(format) {
28
+ if (format === "date") return "date";
29
+ if (format === "time") return "time";
30
+ if (format === "date-time" || format === "datetime") return "datetime-local";
31
+ }
32
+ /**
33
+ * Normalise a dot-separated path into a valid, `sc-` prefixed HTML ID.
34
+ * Dots in paths (from nested objects) become hyphens.
35
+ */
36
+ function fieldId(path) {
37
+ return `sc-${path.replace(/\./g, "-")}`;
38
+ }
27
39
  function renderStringHtml(props) {
28
40
  if (props.readOnly) return serialize(renderStringReadOnly(props));
29
41
  return serialize(renderStringEditable(props));
@@ -52,8 +64,9 @@ function renderStringReadOnly(props) {
52
64
  }
53
65
  function renderStringEditable(props) {
54
66
  const strValue = typeof props.value === "string" ? props.value : "";
55
- const inputType = props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text";
56
- const id = props.path;
67
+ const format = props.constraints.format;
68
+ const inputType = dateInputType(format) ?? (format === "email" ? "email" : format === "uri" ? "url" : "text");
69
+ const id = fieldId(props.path);
57
70
  const attrs = {
58
71
  class: "sc-input",
59
72
  id,
@@ -84,7 +97,7 @@ function renderNumberReadOnly(props) {
84
97
  }
85
98
  function renderNumberEditable(props) {
86
99
  const numValue = typeof props.value === "number" ? String(props.value) : "";
87
- const id = props.path;
100
+ const id = fieldId(props.path);
88
101
  const attrs = {
89
102
  class: "sc-input",
90
103
  id,
@@ -113,7 +126,7 @@ function renderBooleanReadOnly(props) {
113
126
  }, props.value ? "Yes" : "No");
114
127
  }
115
128
  function renderBooleanEditable(props) {
116
- const id = props.path;
129
+ const id = fieldId(props.path);
117
130
  const attrs = {
118
131
  class: "sc-input",
119
132
  id,
@@ -142,7 +155,7 @@ function renderEnumReadOnly(props) {
142
155
  }
143
156
  function renderEnumEditable(props) {
144
157
  const enumValue = typeof props.value === "string" ? props.value : "";
145
- const id = props.path;
158
+ const id = fieldId(props.path);
146
159
  const selectedValue = props.writeOnly ? "" : enumValue;
147
160
  const optionNodes = [h("option", { value: "" }, "Select…"), ...(props.enumValues ?? []).map((v) => {
148
161
  const attrs = { value: v };
@@ -173,7 +186,7 @@ function renderObjectNode(props) {
173
186
  for (const [key, field] of Object.entries(fields)) {
174
187
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
175
188
  const childValue = obj[key];
176
- const childHtml = props.renderChild(field, childValue, key);
189
+ const childHtml = props.renderChild(field, childValue, props.path ? `${props.path}.${key}` : key);
177
190
  children.push(h("dt", { class: "sc-label" }, label));
178
191
  children.push(h("dd", { class: "sc-value" }, raw(childHtml)));
179
192
  }
@@ -186,8 +199,9 @@ function renderObjectNode(props) {
186
199
  for (const [key, field] of Object.entries(fields)) {
187
200
  const label = typeof field.meta.description === "string" ? field.meta.description : key;
188
201
  const fieldId = buildInputId(props.path, key);
202
+ const childPath = props.path ? `${props.path}.${key}` : key;
189
203
  const childValue = obj[key];
190
- const childHtml = props.renderChild(field, childValue, key);
204
+ const childHtml = props.renderChild(field, childValue, childPath);
191
205
  const required = requiredIndicator(field);
192
206
  const labelContent = [label];
193
207
  if (required !== void 0) labelContent.push(required);
@@ -195,7 +209,7 @@ function renderObjectNode(props) {
195
209
  class: "sc-label",
196
210
  for: fieldId
197
211
  }, ...labelContent), raw(childHtml)];
198
- const hint = buildHintElement(key, field.constraints);
212
+ const hint = buildHintElement(fieldId, field.constraints);
199
213
  if (hint !== void 0) fieldChildren.push(hint);
200
214
  children.push(h("div", { class: "sc-field" }, ...fieldChildren));
201
215
  }
@@ -263,6 +277,80 @@ function renderUnionHtml(props) {
263
277
  if (firstOption !== void 0) return props.renderChild(firstOption, props.value);
264
278
  return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
265
279
  }
280
+ function renderDiscriminatedUnionHtml(props) {
281
+ const options = props.options;
282
+ const discriminator = props.discriminator;
283
+ if (options === void 0 || options.length === 0) {
284
+ if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
285
+ return serialize(h("span", { class: "sc-value" }, JSON.stringify(props.value)));
286
+ }
287
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
288
+ const obj = isRecord(props.value) ? props.value : {};
289
+ const discKey = discriminator ?? "";
290
+ const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
291
+ const optionLabels = options.map((opt) => {
292
+ const discriminatorField = opt.fields?.[discKey];
293
+ if (discriminatorField !== void 0) {
294
+ const constVal = discriminatorField.literalValues?.[0];
295
+ if (typeof constVal === "string") return constVal;
296
+ }
297
+ return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
298
+ });
299
+ let activeIndex = 0;
300
+ if (currentDiscriminatorValue !== void 0) {
301
+ const found = optionLabels.indexOf(currentDiscriminatorValue);
302
+ if (found !== -1) activeIndex = found;
303
+ }
304
+ const activeOption = options[activeIndex];
305
+ if (props.readOnly) {
306
+ if (activeOption !== void 0) return props.renderChild(activeOption, props.value);
307
+ return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
308
+ }
309
+ const panelId = `sc-${props.path}-panel`;
310
+ const children = [h("div", {
311
+ role: "tablist",
312
+ class: "sc-tabs",
313
+ "aria-label": "Select variant"
314
+ }, ...options.map((_opt, i) => {
315
+ return h("button", {
316
+ type: "button",
317
+ role: "tab",
318
+ class: i === activeIndex ? "sc-tab sc-tab--active" : "sc-tab",
319
+ id: `sc-${props.path}-tab-${String(i)}`,
320
+ "aria-selected": i === activeIndex ? "true" : void 0,
321
+ "aria-controls": panelId,
322
+ tabindex: i === activeIndex ? "0" : "-1"
323
+ }, optionLabels[i]);
324
+ }))];
325
+ if (activeOption !== void 0) {
326
+ const childHtml = props.renderChild(activeOption, props.value);
327
+ children.push(h("div", {
328
+ role: "tabpanel",
329
+ id: panelId,
330
+ "aria-labelledby": `sc-${props.path}-tab-${String(activeIndex)}`
331
+ }, raw(childHtml)));
332
+ }
333
+ return serialize(h("div", { class: "sc-discriminated-union" }, ...children));
334
+ }
335
+ function renderFileHtml(props) {
336
+ const id = fieldId(props.path);
337
+ const accept = props.constraints.mimeTypes?.join(",");
338
+ if (props.readOnly) return serialize(h("span", {
339
+ class: "sc-value",
340
+ id,
341
+ ...ariaReadonlyAttrs()
342
+ }, "File field"));
343
+ const attrs = {
344
+ class: "sc-input",
345
+ id,
346
+ type: "file",
347
+ name: id
348
+ };
349
+ if (accept !== void 0) attrs.accept = accept;
350
+ Object.assign(attrs, ariaRequiredAttrs(props.tree));
351
+ if (typeof props.meta.description === "string") Object.assign(attrs, ariaLabelAttrs(props.meta.description));
352
+ return serialize(h("input", attrs));
353
+ }
266
354
  function renderUnknownHtml(props) {
267
355
  if (props.readOnly) {
268
356
  if (props.value === void 0 || props.value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
@@ -295,6 +383,8 @@ const defaultHtmlResolver = {
295
383
  record: renderRecordHtml,
296
384
  literal: renderLiteralHtml,
297
385
  union: renderUnionHtml,
386
+ discriminatedUnion: renderDiscriminatedUnionHtml,
387
+ file: renderFileHtml,
298
388
  unknown: renderUnknownHtml
299
389
  };
300
390
  /**
@@ -320,14 +410,15 @@ function renderToHtml(schema, options = {}) {
320
410
  const renderChild = (childTree, childValue, pathSuffix) => {
321
411
  return renderFieldHtml(childTree, childValue, resolver, pathSuffix ?? childTree.meta.description ?? "", renderChild);
322
412
  };
323
- return renderFieldHtml(tree, options.value, resolver, "", renderChild);
413
+ return renderFieldHtml(tree, options.value ?? tree.defaultValue, resolver, "", renderChild);
324
414
  }
325
415
  function renderFieldHtml(tree, value, resolver, path, renderChild) {
416
+ const effectiveValue = value ?? tree.defaultValue;
326
417
  const mergedResolver = mergeHtmlResolvers(resolver, defaultHtmlResolver);
327
418
  const renderFn = getHtmlRenderFn(tree.type, mergedResolver);
328
419
  if (renderFn !== void 0) {
329
420
  const props = {
330
- value,
421
+ value: effectiveValue,
331
422
  readOnly: tree.editability === "presentation",
332
423
  writeOnly: tree.editability === "input",
333
424
  meta: tree.meta,
@@ -345,8 +436,8 @@ function renderFieldHtml(tree, value, resolver, path, renderChild) {
345
436
  if (tree.valueType !== void 0) props.valueType = tree.valueType;
346
437
  return renderFn(props);
347
438
  }
348
- if (value === void 0 || value === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
349
- return serialize(h("span", { class: "sc-value" }, typeof value === "string" ? value : JSON.stringify(value)));
439
+ if (effectiveValue === void 0 || effectiveValue === null) return serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
440
+ return serialize(h("span", { class: "sc-value" }, typeof effectiveValue === "string" ? effectiveValue : JSON.stringify(effectiveValue)));
350
441
  }
351
442
  //#endregion
352
443
  export { defaultHtmlResolver, renderToHtml };
@@ -58,7 +58,7 @@ function* renderToHtmlChunks(schema, options = {}) {
58
58
  const tree = prepareTree(schema, options);
59
59
  const resolver = options.resolver ?? defaultHtmlResolver;
60
60
  const mergedResolver = mergeHtmlResolvers(resolver, defaultHtmlResolver);
61
- yield* streamField(tree, options.value, mergedResolver, "", resolver);
61
+ yield* streamField(tree, options.value ?? tree.defaultValue, mergedResolver, "", resolver);
62
62
  }
63
63
  /**
64
64
  * Render a schema to HTML string chunks asynchronously.
@@ -119,13 +119,18 @@ function prepareTree(schema, options) {
119
119
  });
120
120
  }
121
121
  function* streamField(tree, value, mergedResolver, path, rawResolver) {
122
+ const effectiveValue = value ?? tree.defaultValue;
122
123
  const type = tree.type;
123
124
  if (type === "string" || type === "number" || type === "boolean" || type === "enum" || type === "literal" || type === "file" || type === "unknown") {
124
- yield renderLeaf(tree, value, mergedResolver, path);
125
+ yield renderLeaf(tree, effectiveValue, mergedResolver, path);
125
126
  return;
126
127
  }
127
- if (type === "union" || type === "discriminatedUnion") {
128
- yield* streamUnion(tree, value, mergedResolver, path, rawResolver);
128
+ if (type === "union") {
129
+ yield* streamUnion(tree, effectiveValue, mergedResolver, path, rawResolver);
130
+ return;
131
+ }
132
+ if (type === "discriminatedUnion") {
133
+ yield* streamDiscriminatedUnion(tree, value, mergedResolver, path, rawResolver);
129
134
  return;
130
135
  }
131
136
  if (type === "object") {
@@ -243,6 +248,64 @@ function* streamUnion(tree, value, mergedResolver, path, rawResolver) {
243
248
  if (target !== void 0) yield* streamField(target, value, mergedResolver, typeof target.meta.description === "string" ? target.meta.description : "", rawResolver);
244
249
  else yield serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
245
250
  }
251
+ function* streamDiscriminatedUnion(tree, value, mergedResolver, path, rawResolver) {
252
+ const options = tree.options;
253
+ const discriminator = tree.discriminator;
254
+ if (options === void 0 || options.length === 0) {
255
+ if (value === void 0 || value === null) yield serialize(h("span", { class: "sc-value sc-value--empty" }, "—"));
256
+ else yield serialize(h("span", { class: "sc-value" }, JSON.stringify(value)));
257
+ return;
258
+ }
259
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
260
+ const obj = isRecord(value) ? value : {};
261
+ const discKey = discriminator ?? "";
262
+ const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
263
+ const optionLabels = options.map((opt) => {
264
+ const discriminatorField = opt.fields?.[discKey];
265
+ if (discriminatorField !== void 0) {
266
+ const constVal = discriminatorField.literalValues?.[0];
267
+ if (typeof constVal === "string") return constVal;
268
+ }
269
+ return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
270
+ });
271
+ let activeIndex = 0;
272
+ if (currentDiscriminatorValue !== void 0) {
273
+ const found = optionLabels.indexOf(currentDiscriminatorValue);
274
+ if (found !== -1) activeIndex = found;
275
+ }
276
+ const activeOption = options[activeIndex];
277
+ if (tree.editability === "presentation") {
278
+ if (activeOption !== void 0) yield* streamField(activeOption, value, mergedResolver, typeof activeOption.meta.description === "string" ? activeOption.meta.description : "", rawResolver);
279
+ return;
280
+ }
281
+ const panelId = `sc-${path}-panel`;
282
+ const wrapper = h("div", { class: "sc-discriminated-union" });
283
+ yield yieldOpen(wrapper);
284
+ yield serialize(h("div", {
285
+ role: "tablist",
286
+ class: "sc-tabs",
287
+ "aria-label": "Select variant"
288
+ }, ...options.map((_opt, i) => {
289
+ return h("button", {
290
+ type: "button",
291
+ role: "tab",
292
+ class: i === activeIndex ? "sc-tab sc-tab--active" : "sc-tab",
293
+ id: `sc-${path}-tab-${String(i)}`,
294
+ "aria-selected": i === activeIndex ? "true" : void 0,
295
+ "aria-controls": panelId,
296
+ tabindex: i === activeIndex ? "0" : "-1"
297
+ }, optionLabels[i]);
298
+ })));
299
+ const panelOpen = h("div", {
300
+ role: "tabpanel",
301
+ id: panelId,
302
+ "aria-labelledby": `sc-${path}-tab-${String(activeIndex)}`
303
+ });
304
+ yield yieldOpen(panelOpen);
305
+ if (activeOption !== void 0) yield* streamField(activeOption, value, mergedResolver, typeof activeOption.meta.description === "string" ? activeOption.meta.description : "", rawResolver);
306
+ yield yieldClose(panelOpen);
307
+ yield yieldClose(wrapper);
308
+ }
246
309
  function renderLeaf(tree, value, mergedResolver, path) {
247
310
  const renderFn = getHtmlRenderFn(tree.type, mergedResolver);
248
311
  if (renderFn !== void 0) {
@@ -79,6 +79,12 @@
79
79
  margin: 0;
80
80
  }
81
81
 
82
+ .sc-input[type="file"] {
83
+ padding: 0.25rem 0;
84
+ border: none;
85
+ background: none;
86
+ }
87
+
82
88
  /* ---------------------------------------------------------------------- */
83
89
  /* Read-only values */
84
90
  /* ---------------------------------------------------------------------- */
@@ -149,3 +155,40 @@
149
155
  color: #6b7280;
150
156
  margin-top: 0.25rem;
151
157
  }
158
+
159
+ /* ---------------------------------------------------------------------- */
160
+ /* Discriminated union tabs */
161
+ /* ---------------------------------------------------------------------- */
162
+
163
+ .sc-discriminated-union {
164
+ border: 1px solid #e2e8f0;
165
+ border-radius: 0.375rem;
166
+ padding: 1rem;
167
+ }
168
+
169
+ .sc-tabs {
170
+ display: flex;
171
+ gap: 0.25rem;
172
+ margin-bottom: 0.75rem;
173
+ }
174
+
175
+ .sc-tab {
176
+ padding: 0.25rem 0.75rem;
177
+ border: 1px solid #d1d5db;
178
+ border-radius: 0.25rem;
179
+ background: transparent;
180
+ cursor: pointer;
181
+ font-size: 0.875rem;
182
+ color: #374151;
183
+ }
184
+
185
+ .sc-tab:focus-visible {
186
+ outline: 2px solid #3b82f6;
187
+ outline-offset: 2px;
188
+ }
189
+
190
+ .sc-tab--active {
191
+ border-color: #3b82f6;
192
+ background: #eff6ff;
193
+ color: #1d4ed8;
194
+ }
@@ -1,4 +1,4 @@
1
- import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-BU0ETFHk.mjs";
1
+ import { c as InferResponseFields, m as SchemaMeta, o as InferParameterOverrides, r as FieldOverride, s as InferRequestBodyFields } from "../types-DDCD6Xnx.mjs";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/openapi/components.d.ts
@@ -1,4 +1,4 @@
1
- import { l as JsonObject } from "../types-BU0ETFHk.mjs";
1
+ import { l as JsonObject } from "../types-DDCD6Xnx.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-BU0ETFHk.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-DDCD6Xnx.mjs";
2
2
  import { t as SchemaError } from "../errors-DIKI2C78.mjs";
3
3
  import { z } from "zod";
4
4
  import { ReactNode } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { isObject, toRecord } from "../core/guards.mjs";
2
3
  import { normaliseSchema } from "../core/adapter.mjs";
3
4
  import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
@@ -83,7 +84,7 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
83
84
  const renderChild = (childTree, childValue, childOnChange) => {
84
85
  return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
85
86
  };
86
- return renderField(tree, value, handleChange, userResolver, renderChild);
87
+ return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild);
87
88
  }
88
89
  function runValidation(zodSchema, jsonSchema, value, onError) {
89
90
  if (zodSchema !== void 0 && isObject(zodSchema)) {
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { SchemaError } from "../core/errors.mjs";
2
3
  import { Component } from "react";
3
4
  //#region src/react/SchemaErrorBoundary.tsx