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 +39 -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
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
let MuiTextField = (props) => /* @__PURE__ */ jsx("input", { ...props });
|
|
178
|
+
let MuiCheckbox = (props) => /* @__PURE__ */ jsx("input", {
|
|
179
|
+
type: "checkbox",
|
|
180
|
+
...props
|
|
181
|
+
});
|
|
182
|
+
let MuiTypography = (props) => /* @__PURE__ */ jsx("span", { ...props });
|
|
183
|
+
let MuiBox = (props) => /* @__PURE__ */ jsx("div", { ...props });
|
|
184
|
+
let MuiMenuItem = (props) => /* @__PURE__ */ jsx("option", { ...props });
|
|
185
|
+
let MuiFormControlLabel = (props) => {
|
|
186
|
+
const { control, label, ...rest } = props;
|
|
187
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
188
|
+
...rest,
|
|
189
|
+
children: [isValidElement(control) ? control : null, typeof label === "string" ? label : null]
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Register real MUI components. Call once at app startup.
|
|
194
|
+
*/
|
|
195
|
+
function registerMuiComponents(components) {
|
|
196
|
+
MuiTextField = components.TextField;
|
|
197
|
+
MuiCheckbox = components.Checkbox;
|
|
198
|
+
MuiTypography = components.Typography;
|
|
199
|
+
MuiBox = components.Box;
|
|
200
|
+
MuiMenuItem = components.MenuItem;
|
|
201
|
+
MuiFormControlLabel = components.FormControlLabel;
|
|
202
|
+
}
|
|
203
|
+
function buildResolver() {
|
|
204
|
+
const resolver = {
|
|
205
|
+
string: renderStringInput,
|
|
206
|
+
number: renderNumberInput,
|
|
207
|
+
boolean: renderBooleanInput,
|
|
208
|
+
enum: renderEnumInput,
|
|
209
|
+
object: renderObjectContainer,
|
|
210
|
+
array: renderArrayContainer
|
|
211
|
+
};
|
|
212
|
+
if (headlessResolver.literal !== void 0) resolver.literal = headlessResolver.literal;
|
|
213
|
+
if (headlessResolver.union !== void 0) resolver.union = headlessResolver.union;
|
|
214
|
+
if (headlessResolver.discriminatedUnion !== void 0) resolver.discriminatedUnion = headlessResolver.discriminatedUnion;
|
|
215
|
+
if (headlessResolver.record !== void 0) resolver.record = headlessResolver.record;
|
|
216
|
+
if (headlessResolver.file !== void 0) resolver.file = headlessResolver.file;
|
|
217
|
+
if (headlessResolver.unknown !== void 0) resolver.unknown = headlessResolver.unknown;
|
|
218
|
+
return resolver;
|
|
219
|
+
}
|
|
220
|
+
const muiResolver = buildResolver();
|
|
221
|
+
//#endregion
|
|
222
|
+
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.2.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,7 +40,8 @@
|
|
|
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",
|
|
@@ -88,10 +89,11 @@
|
|
|
88
89
|
"@semantic-release/changelog": "6.0.3",
|
|
89
90
|
"@semantic-release/git": "10.0.1",
|
|
90
91
|
"@semantic-release/github": "12.0.6",
|
|
91
|
-
"@storybook/react": "10.
|
|
92
|
-
"@storybook/react-vite": "10.
|
|
92
|
+
"@storybook/react": "10.3.6",
|
|
93
|
+
"@storybook/react-vite": "10.3.6",
|
|
93
94
|
"@types/node": "25.6.0",
|
|
94
95
|
"@types/react": "19.2.14",
|
|
96
|
+
"@types/react-dom": "19.2.3",
|
|
95
97
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
96
98
|
"eslint": "10.3.0",
|
|
97
99
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -102,13 +104,13 @@
|
|
|
102
104
|
"react": "19.2.6",
|
|
103
105
|
"react-dom": "19.2.6",
|
|
104
106
|
"semantic-release": "25.0.3",
|
|
105
|
-
"storybook": "10.
|
|
106
|
-
"tsdown": "0.
|
|
107
|
+
"storybook": "10.3.6",
|
|
108
|
+
"tsdown": "0.22.0",
|
|
107
109
|
"tslib": "2.8.1",
|
|
108
110
|
"turbo": "2.9.9",
|
|
109
111
|
"typescript": "6.0.3",
|
|
110
112
|
"typescript-eslint": "8.59.2",
|
|
111
|
-
"vite": "^8.0.
|
|
113
|
+
"vite": "^8.0.11",
|
|
112
114
|
"zod": "4.4.3"
|
|
113
115
|
},
|
|
114
116
|
"packageManager": "pnpm@10.33.1"
|