schema-components 1.1.0 → 1.3.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 +46 -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 +227 -0
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/{types-BU0ETFHk.d.mts → types-DDCD6Xnx.d.mts} +3 -1
- package/package.json +18 -7
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { headlessResolver, toReactNode } from "../react/headless.mjs";
|
|
3
|
+
import { isValidElement } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/themes/mui.tsx
|
|
6
|
+
function ariaRequired(tree) {
|
|
7
|
+
return { required: tree.isOptional === false };
|
|
8
|
+
}
|
|
9
|
+
function renderStringInput(props) {
|
|
10
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
11
|
+
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
12
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(MuiTypography, {
|
|
13
|
+
variant: "body2",
|
|
14
|
+
children: strValue || "—"
|
|
15
|
+
});
|
|
16
|
+
return /* @__PURE__ */ jsx(MuiTextField, {
|
|
17
|
+
label,
|
|
18
|
+
type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
|
|
19
|
+
value: props.writeOnly ? "" : strValue,
|
|
20
|
+
onChange: (e) => {
|
|
21
|
+
props.onChange(e.target.value);
|
|
22
|
+
},
|
|
23
|
+
fullWidth: true,
|
|
24
|
+
size: "small",
|
|
25
|
+
variant: "outlined",
|
|
26
|
+
inputProps: {
|
|
27
|
+
minLength: props.constraints.minLength,
|
|
28
|
+
maxLength: props.constraints.maxLength
|
|
29
|
+
},
|
|
30
|
+
...ariaRequired(props.tree)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function renderNumberInput(props) {
|
|
34
|
+
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
35
|
+
if (props.readOnly) {
|
|
36
|
+
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(MuiTypography, {
|
|
37
|
+
variant: "body2",
|
|
38
|
+
children: "—"
|
|
39
|
+
});
|
|
40
|
+
return /* @__PURE__ */ jsx(MuiTypography, {
|
|
41
|
+
variant: "body2",
|
|
42
|
+
children: props.value.toLocaleString()
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return /* @__PURE__ */ jsx(MuiTextField, {
|
|
46
|
+
label,
|
|
47
|
+
type: "number",
|
|
48
|
+
value: typeof props.value === "number" ? props.value : "",
|
|
49
|
+
onChange: (e) => {
|
|
50
|
+
props.onChange(Number(e.target.value));
|
|
51
|
+
},
|
|
52
|
+
fullWidth: true,
|
|
53
|
+
size: "small",
|
|
54
|
+
variant: "outlined",
|
|
55
|
+
inputProps: {
|
|
56
|
+
min: props.constraints.minimum,
|
|
57
|
+
max: props.constraints.maximum
|
|
58
|
+
},
|
|
59
|
+
...ariaRequired(props.tree)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function renderBooleanInput(props) {
|
|
63
|
+
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
64
|
+
if (props.readOnly) {
|
|
65
|
+
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(MuiTypography, {
|
|
66
|
+
variant: "body2",
|
|
67
|
+
children: "—"
|
|
68
|
+
});
|
|
69
|
+
return /* @__PURE__ */ jsx(MuiTypography, {
|
|
70
|
+
variant: "body2",
|
|
71
|
+
children: props.value ? "Yes" : "No"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return /* @__PURE__ */ jsx(MuiFormControlLabel, {
|
|
75
|
+
control: /* @__PURE__ */ jsx(MuiCheckbox, {
|
|
76
|
+
checked: props.value === true,
|
|
77
|
+
onChange: (e) => {
|
|
78
|
+
props.onChange(e.target.checked);
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
label
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function renderEnumInput(props) {
|
|
85
|
+
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
86
|
+
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
87
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(MuiTypography, {
|
|
88
|
+
variant: "body2",
|
|
89
|
+
children: enumValue || "—"
|
|
90
|
+
});
|
|
91
|
+
return /* @__PURE__ */ jsxs(MuiTextField, {
|
|
92
|
+
select: true,
|
|
93
|
+
label,
|
|
94
|
+
value: props.writeOnly ? "" : enumValue,
|
|
95
|
+
onChange: (e) => {
|
|
96
|
+
props.onChange(e.target.value);
|
|
97
|
+
},
|
|
98
|
+
fullWidth: true,
|
|
99
|
+
size: "small",
|
|
100
|
+
variant: "outlined",
|
|
101
|
+
...ariaRequired(props.tree),
|
|
102
|
+
children: [/* @__PURE__ */ jsxs(MuiMenuItem, {
|
|
103
|
+
value: "",
|
|
104
|
+
children: ["Select", "…"]
|
|
105
|
+
}), (props.enumValues ?? []).map((v) => /* @__PURE__ */ jsx(MuiMenuItem, {
|
|
106
|
+
value: v,
|
|
107
|
+
children: v
|
|
108
|
+
}, v))]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function renderObjectContainer(props) {
|
|
112
|
+
const fields = props.fields;
|
|
113
|
+
if (fields === void 0) return null;
|
|
114
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
115
|
+
return /* @__PURE__ */ jsxs(MuiBox, {
|
|
116
|
+
sx: {
|
|
117
|
+
display: "flex",
|
|
118
|
+
flexDirection: "column",
|
|
119
|
+
gap: 2
|
|
120
|
+
},
|
|
121
|
+
children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx(MuiTypography, {
|
|
122
|
+
variant: "h6",
|
|
123
|
+
children: props.meta.description
|
|
124
|
+
}), Object.entries(fields).map(([key, field]) => {
|
|
125
|
+
const childValue = obj[key];
|
|
126
|
+
const childOnChange = (v) => {
|
|
127
|
+
const updated = {};
|
|
128
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
129
|
+
updated[key] = v;
|
|
130
|
+
props.onChange(updated);
|
|
131
|
+
};
|
|
132
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(field, childValue, childOnChange)) }, key);
|
|
133
|
+
})]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function renderArrayContainer(props) {
|
|
137
|
+
const arr = Array.isArray(props.value) ? props.value : [];
|
|
138
|
+
const element = props.element;
|
|
139
|
+
if (element === void 0) return null;
|
|
140
|
+
return /* @__PURE__ */ jsx(MuiBox, {
|
|
141
|
+
sx: {
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
gap: 1
|
|
145
|
+
},
|
|
146
|
+
children: arr.map((item, i) => {
|
|
147
|
+
const childOnChange = (v) => {
|
|
148
|
+
const next = arr.slice();
|
|
149
|
+
next[i] = v;
|
|
150
|
+
props.onChange(next);
|
|
151
|
+
};
|
|
152
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* MUI components are not bundled with this adapter.
|
|
158
|
+
* Instead, the resolver uses thin wrapper components that delegate to
|
|
159
|
+
* the consuming project's MUI installation.
|
|
160
|
+
*
|
|
161
|
+
* This avoids a hard dependency on @mui/material while providing
|
|
162
|
+
* type-safe rendering. If MUI is not installed, these wrappers
|
|
163
|
+
* render basic HTML elements as fallback.
|
|
164
|
+
*
|
|
165
|
+
* To use real MUI components, wrap your app with MuiProvider:
|
|
166
|
+
* import { MuiProvider } from "schema-components/themes/mui";
|
|
167
|
+
* import { TextField, Checkbox, ... } from "@mui/material";
|
|
168
|
+
*
|
|
169
|
+
* <MuiProvider
|
|
170
|
+
* TextField={TextField}
|
|
171
|
+
* Checkbox={Checkbox}
|
|
172
|
+
* ...
|
|
173
|
+
* >
|
|
174
|
+
* <SchemaComponent ... />
|
|
175
|
+
* </MuiProvider>
|
|
176
|
+
*/
|
|
177
|
+
function stripChildren(props) {
|
|
178
|
+
const rest = { ...props };
|
|
179
|
+
if ("children" in rest) delete rest.children;
|
|
180
|
+
return rest;
|
|
181
|
+
}
|
|
182
|
+
let MuiTextField = (props) => /* @__PURE__ */ jsx("input", { ...stripChildren(props) });
|
|
183
|
+
let MuiCheckbox = (props) => /* @__PURE__ */ jsx("input", {
|
|
184
|
+
type: "checkbox",
|
|
185
|
+
...stripChildren(props)
|
|
186
|
+
});
|
|
187
|
+
let MuiTypography = (props) => /* @__PURE__ */ jsx("span", { ...props });
|
|
188
|
+
let MuiBox = (props) => /* @__PURE__ */ jsx("div", { ...props });
|
|
189
|
+
let MuiMenuItem = (props) => /* @__PURE__ */ jsx("option", { ...props });
|
|
190
|
+
let MuiFormControlLabel = (props) => {
|
|
191
|
+
const { control, label, ...rest } = props;
|
|
192
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
193
|
+
...rest,
|
|
194
|
+
children: [isValidElement(control) ? control : null, typeof label === "string" ? label : null]
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Register real MUI components. Call once at app startup.
|
|
199
|
+
*/
|
|
200
|
+
function registerMuiComponents(components) {
|
|
201
|
+
MuiTextField = components.TextField;
|
|
202
|
+
MuiCheckbox = components.Checkbox;
|
|
203
|
+
MuiTypography = components.Typography;
|
|
204
|
+
MuiBox = components.Box;
|
|
205
|
+
MuiMenuItem = components.MenuItem;
|
|
206
|
+
MuiFormControlLabel = components.FormControlLabel;
|
|
207
|
+
}
|
|
208
|
+
function buildResolver() {
|
|
209
|
+
const resolver = {
|
|
210
|
+
string: renderStringInput,
|
|
211
|
+
number: renderNumberInput,
|
|
212
|
+
boolean: renderBooleanInput,
|
|
213
|
+
enum: renderEnumInput,
|
|
214
|
+
object: renderObjectContainer,
|
|
215
|
+
array: renderArrayContainer
|
|
216
|
+
};
|
|
217
|
+
if (headlessResolver.literal !== void 0) resolver.literal = headlessResolver.literal;
|
|
218
|
+
if (headlessResolver.union !== void 0) resolver.union = headlessResolver.union;
|
|
219
|
+
if (headlessResolver.discriminatedUnion !== void 0) resolver.discriminatedUnion = headlessResolver.discriminatedUnion;
|
|
220
|
+
if (headlessResolver.record !== void 0) resolver.record = headlessResolver.record;
|
|
221
|
+
if (headlessResolver.file !== void 0) resolver.file = headlessResolver.file;
|
|
222
|
+
if (headlessResolver.unknown !== void 0) resolver.unknown = headlessResolver.unknown;
|
|
223
|
+
return resolver;
|
|
224
|
+
}
|
|
225
|
+
const muiResolver = buildResolver();
|
|
226
|
+
//#endregion
|
|
227
|
+
export { muiResolver, registerMuiComponents };
|
package/dist/themes/shadcn.d.mts
CHANGED
|
@@ -75,6 +75,7 @@ interface ComponentResolver {
|
|
|
75
75
|
array?: RenderFunction;
|
|
76
76
|
record?: RenderFunction;
|
|
77
77
|
union?: RenderFunction;
|
|
78
|
+
discriminatedUnion?: RenderFunction;
|
|
78
79
|
literal?: RenderFunction;
|
|
79
80
|
file?: RenderFunction;
|
|
80
81
|
unknown?: RenderFunction;
|
|
@@ -94,11 +95,12 @@ interface HtmlResolver {
|
|
|
94
95
|
array?: HtmlRenderFunction;
|
|
95
96
|
record?: HtmlRenderFunction;
|
|
96
97
|
union?: HtmlRenderFunction;
|
|
98
|
+
discriminatedUnion?: HtmlRenderFunction;
|
|
97
99
|
literal?: HtmlRenderFunction;
|
|
98
100
|
file?: HtmlRenderFunction;
|
|
99
101
|
unknown?: HtmlRenderFunction;
|
|
100
102
|
}
|
|
101
|
-
declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "enum", "object", "array", "record", "union", "literal", "file", "unknown"];
|
|
103
|
+
declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "enum", "object", "array", "record", "union", "discriminatedUnion", "literal", "file", "unknown"];
|
|
102
104
|
type ResolverKey = (typeof RESOLVER_KEYS)[number];
|
|
103
105
|
/**
|
|
104
106
|
* Map a schema type to the resolver key that handles it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "schema-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -40,12 +40,14 @@
|
|
|
40
40
|
"_lint": "eslint --cache 'src/**/*.{ts,tsx}'",
|
|
41
41
|
"_lint:fix": "eslint --cache --fix 'src/**/*.{ts,tsx}'",
|
|
42
42
|
"_test": "node --test 'tests/**/*.unit.test.ts' 'tests/**/*.integration.test.ts'",
|
|
43
|
-
"_test:
|
|
43
|
+
"_test:ssr": "node --test 'tests/ssr.e2e.test.ts'",
|
|
44
|
+
"_test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=80 --test-coverage-include='src/**/*.ts' --test-coverage-include='src/**/*.tsx' 'tests/**/*.unit.test.ts' 'tests/**/*.integration.test.ts'",
|
|
44
45
|
"_build": "tsdown && cp src/html/styles.css dist/html/styles.css",
|
|
45
46
|
"typecheck": "turbo run _typecheck",
|
|
46
47
|
"lint": "turbo run _lint",
|
|
47
48
|
"lint:fix": "turbo run _lint:fix",
|
|
48
49
|
"test": "turbo run _test",
|
|
50
|
+
"test-storybook": "vitest run --project=storybook",
|
|
49
51
|
"test:coverage": "turbo run _test:coverage",
|
|
50
52
|
"check": "turbo run _check",
|
|
51
53
|
"validate": "turbo run _validate",
|
|
@@ -85,30 +87,39 @@
|
|
|
85
87
|
"@commitlint/cli": "20.5.3",
|
|
86
88
|
"@commitlint/config-conventional": "20.5.3",
|
|
87
89
|
"@eslint/js": "10.0.1",
|
|
90
|
+
"@playwright/test": "1.59.1",
|
|
88
91
|
"@semantic-release/changelog": "6.0.3",
|
|
89
92
|
"@semantic-release/git": "10.0.1",
|
|
90
93
|
"@semantic-release/github": "12.0.6",
|
|
91
|
-
"@storybook/
|
|
92
|
-
"@storybook/
|
|
94
|
+
"@storybook/addon-a11y": "10.3.6",
|
|
95
|
+
"@storybook/addon-vitest": "10.3.6",
|
|
96
|
+
"@storybook/react": "10.3.6",
|
|
97
|
+
"@storybook/react-vite": "10.3.6",
|
|
93
98
|
"@types/node": "25.6.0",
|
|
94
99
|
"@types/react": "19.2.14",
|
|
100
|
+
"@types/react-dom": "19.2.3",
|
|
101
|
+
"@vitest/browser": "4.1.5",
|
|
102
|
+
"@vitest/browser-playwright": "4.1.5",
|
|
103
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
95
104
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
96
105
|
"eslint": "10.3.0",
|
|
97
106
|
"eslint-config-prettier": "10.1.8",
|
|
98
107
|
"eslint-plugin-prettier": "5.5.5",
|
|
99
108
|
"husky": "9.1.7",
|
|
100
109
|
"lint-staged": "17.0.2",
|
|
110
|
+
"playwright": "^1.59.1",
|
|
101
111
|
"prettier": "3.8.3",
|
|
102
112
|
"react": "19.2.6",
|
|
103
113
|
"react-dom": "19.2.6",
|
|
104
114
|
"semantic-release": "25.0.3",
|
|
105
|
-
"storybook": "10.
|
|
106
|
-
"tsdown": "0.
|
|
115
|
+
"storybook": "10.3.6",
|
|
116
|
+
"tsdown": "0.22.0",
|
|
107
117
|
"tslib": "2.8.1",
|
|
108
118
|
"turbo": "2.9.9",
|
|
109
119
|
"typescript": "6.0.3",
|
|
110
120
|
"typescript-eslint": "8.59.2",
|
|
111
|
-
"vite": "^8.0.
|
|
121
|
+
"vite": "^8.0.11",
|
|
122
|
+
"vitest": "4.1.5",
|
|
112
123
|
"zod": "4.4.3"
|
|
113
124
|
},
|
|
114
125
|
"packageManager": "pnpm@10.33.1"
|