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.
@@ -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 };
@@ -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.1.0",
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: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",
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/react": "10.4.0",
92
- "@storybook/react-vite": "10.4.0",
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.4.0",
106
- "tsdown": "0.21.10",
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.12",
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"