schema-components 1.1.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 +32 -0
- package/README.md +102 -22
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/renderer.d.mts +1 -1
- package/dist/core/renderer.mjs +2 -1
- package/dist/core/types.d.mts +1 -1
- package/dist/core/walker.d.mts +1 -1
- package/dist/html/a11y.d.mts +1 -1
- package/dist/html/renderToHtml.d.mts +1 -1
- package/dist/html/renderToHtml.mjs +103 -12
- package/dist/html/renderToHtmlStream.mjs +67 -4
- package/dist/html/styles.css +43 -0
- package/dist/openapi/components.d.mts +1 -1
- package/dist/openapi/parser.d.mts +1 -1
- package/dist/react/SchemaComponent.d.mts +1 -1
- package/dist/react/SchemaComponent.mjs +2 -1
- package/dist/react/SchemaErrorBoundary.mjs +1 -0
- package/dist/react/SchemaView.d.mts +41 -0
- package/dist/react/SchemaView.mjs +102 -0
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headless.mjs +339 -24
- package/dist/themes/mui.d.mts +17 -0
- package/dist/themes/mui.mjs +222 -0
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/{types-BU0ETFHk.d.mts → types-DDCD6Xnx.d.mts} +3 -1
- package/package.json +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
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
|
+
|
|
1
33
|
## [1.1.0](https://github.com/Mearman/schema-components/compare/v1.0.0...v1.1.0) (2026-05-14)
|
|
2
34
|
|
|
3
35
|
### 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
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
Every module is imported directly — no barrel files.
|
|
597
|
+
Every module is imported directly — no barrel files. Organised exports:
|
|
508
598
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
```
|
package/dist/core/adapter.d.mts
CHANGED
package/dist/core/renderer.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/core/renderer.mjs
CHANGED
|
@@ -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
|
}
|
package/dist/core/types.d.mts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/core/walker.d.mts
CHANGED
package/dist/html/a11y.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as HtmlResolver, S as HtmlRenderProps, m as SchemaMeta, x as HtmlRenderFunction } from "../types-
|
|
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
|
|
56
|
-
const
|
|
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,
|
|
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(
|
|
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 (
|
|
349
|
-
return serialize(h("span", { class: "sc-value" }, typeof
|
|
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,
|
|
125
|
+
yield renderLeaf(tree, effectiveValue, mergedResolver, path);
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
|
-
if (type === "union"
|
|
128
|
-
yield* streamUnion(tree,
|
|
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) {
|
package/dist/html/styles.css
CHANGED
|
@@ -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-
|
|
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 { 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-
|
|
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)) {
|