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.
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-BU0ETFHk.mjs";
1
+ import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
2
2
 
3
3
  //#region src/themes/shadcn.d.ts
4
4
  declare const shadcnResolver: ComponentResolver;
@@ -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.0.0",
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:coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-branches=80 --test-coverage-functions=80 --test-coverage-include='src/**/*.ts' --test-coverage-include='src/**/*.tsx' 'tests/**/*.unit.test.ts' 'tests/**/*.integration.test.ts'",
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.4.0",
92
- "@storybook/react-vite": "10.4.0",
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.4.0",
106
- "tsdown": "0.21.10",
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.12",
113
+ "vite": "^8.0.11",
112
114
  "zod": "4.4.3"
113
115
  },
114
116
  "packageManager": "pnpm@10.33.1"