schema-components 0.0.0 → 1.0.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 +52 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/dist/core/adapter.d.mts +19 -0
- package/dist/core/adapter.mjs +140 -0
- package/dist/core/errors.d.mts +2 -0
- package/dist/core/errors.mjs +74 -0
- package/dist/core/guards.d.mts +44 -0
- package/dist/core/guards.mjs +58 -0
- package/dist/core/renderer.d.mts +2 -0
- package/dist/core/renderer.mjs +71 -0
- package/dist/core/types.d.mts +3 -0
- package/dist/core/types.mjs +40 -0
- package/dist/core/walker.d.mts +14 -0
- package/dist/core/walker.mjs +366 -0
- package/dist/errors-DIKI2C78.d.mts +57 -0
- package/dist/html/a11y.d.mts +47 -0
- package/dist/html/a11y.mjs +81 -0
- package/dist/html/html.d.mts +135 -0
- package/dist/html/html.mjs +168 -0
- package/dist/html/renderToHtml.d.mts +32 -0
- package/dist/html/renderToHtml.mjs +352 -0
- package/dist/html/renderToHtmlStream.d.mts +58 -0
- package/dist/html/renderToHtmlStream.mjs +285 -0
- package/dist/html/styles.css +151 -0
- package/dist/openapi/components.d.mts +76 -0
- package/dist/openapi/components.mjs +223 -0
- package/dist/openapi/parser.d.mts +45 -0
- package/dist/openapi/parser.mjs +159 -0
- package/dist/react/SchemaComponent.d.mts +96 -0
- package/dist/react/SchemaComponent.mjs +283 -0
- package/dist/react/SchemaErrorBoundary.d.mts +26 -0
- package/dist/react/SchemaErrorBoundary.mjs +47 -0
- package/dist/react/headless.d.mts +13 -0
- package/dist/react/headless.mjs +163 -0
- package/dist/themes/shadcn.d.mts +6 -0
- package/dist/themes/shadcn.mjs +166 -0
- package/dist/types-BU0ETFHk.d.mts +326 -0
- package/package.json +113 -3
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { isObject, toRecord } from "../core/guards.mjs";
|
|
2
|
+
import { normaliseSchema } from "../core/adapter.mjs";
|
|
3
|
+
import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
4
|
+
import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
5
|
+
import { walk } from "../core/walker.mjs";
|
|
6
|
+
import { headlessResolver } from "./headless.mjs";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
//#region src/react/SchemaComponent.tsx
|
|
11
|
+
/**
|
|
12
|
+
* <SchemaComponent> — renders UI from Zod, JSON Schema, or OpenAPI schemas.
|
|
13
|
+
*
|
|
14
|
+
* Auto-detects the input format, normalises to JSON Schema via the adapter,
|
|
15
|
+
* walks the JSON Schema tree, and delegates rendering to the
|
|
16
|
+
* ComponentResolver (theme adapter). Falls back to headless HTML.
|
|
17
|
+
*
|
|
18
|
+
* The `fields` prop type is inferred from the `schema` prop:
|
|
19
|
+
* - Zod schemas → FieldOverrides<z.infer<T>> (full autocomplete)
|
|
20
|
+
* - JSON Schema `as const` → FieldOverrides<FromJSONSchema<T>> (full autocomplete)
|
|
21
|
+
* - OpenAPI `as const` + `ref` → FieldOverrides<ResolveOpenAPIRef<T, Ref>>
|
|
22
|
+
* - Runtime schemas → Record<string, FieldOverride> (no autocomplete)
|
|
23
|
+
*/
|
|
24
|
+
const UserResolverContext = createContext(void 0);
|
|
25
|
+
function SchemaProvider({ resolver, children }) {
|
|
26
|
+
return /* @__PURE__ */ jsx(UserResolverContext.Provider, {
|
|
27
|
+
value: resolver,
|
|
28
|
+
children
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const widgetRegistry = /* @__PURE__ */ new Map();
|
|
32
|
+
function registerWidget(name, render) {
|
|
33
|
+
widgetRegistry.set(name, render);
|
|
34
|
+
}
|
|
35
|
+
function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, fields, meta: componentMeta, readOnly, writeOnly, description }) {
|
|
36
|
+
const userResolver = useContext(UserResolverContext);
|
|
37
|
+
const mergedMeta = useMemo(() => {
|
|
38
|
+
const merged = { ...componentMeta };
|
|
39
|
+
if (readOnly === true) merged.readOnly = true;
|
|
40
|
+
if (writeOnly === true) merged.writeOnly = true;
|
|
41
|
+
if (description !== void 0) merged.description = description;
|
|
42
|
+
return merged;
|
|
43
|
+
}, [
|
|
44
|
+
componentMeta,
|
|
45
|
+
readOnly,
|
|
46
|
+
writeOnly,
|
|
47
|
+
description
|
|
48
|
+
]);
|
|
49
|
+
let jsonSchema;
|
|
50
|
+
let zodSchema;
|
|
51
|
+
let rootMeta;
|
|
52
|
+
let rootDocument;
|
|
53
|
+
try {
|
|
54
|
+
const normalised = normaliseSchema(schemaInput, refInput);
|
|
55
|
+
jsonSchema = normalised.jsonSchema;
|
|
56
|
+
zodSchema = normalised.zodSchema;
|
|
57
|
+
rootMeta = normalised.rootMeta;
|
|
58
|
+
rootDocument = normalised.rootDocument;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const error = new SchemaNormalisationError(err instanceof Error ? err.message : "Failed to normalise schema", schemaInput, detectNormalisationKind(err));
|
|
61
|
+
if (onError !== void 0) {
|
|
62
|
+
onError(error);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const handleChange = useCallback((nextValue) => {
|
|
68
|
+
if (validate) runValidation(zodSchema, jsonSchema, nextValue, onValidationError);
|
|
69
|
+
onChange?.(nextValue);
|
|
70
|
+
}, [
|
|
71
|
+
validate,
|
|
72
|
+
zodSchema,
|
|
73
|
+
jsonSchema,
|
|
74
|
+
onChange,
|
|
75
|
+
onValidationError
|
|
76
|
+
]);
|
|
77
|
+
const tree = walk(jsonSchema, {
|
|
78
|
+
componentMeta: mergedMeta,
|
|
79
|
+
rootMeta,
|
|
80
|
+
fieldOverrides: fields,
|
|
81
|
+
rootDocument
|
|
82
|
+
});
|
|
83
|
+
const renderChild = (childTree, childValue, childOnChange) => {
|
|
84
|
+
return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
|
|
85
|
+
};
|
|
86
|
+
return renderField(tree, value, handleChange, userResolver, renderChild);
|
|
87
|
+
}
|
|
88
|
+
function runValidation(zodSchema, jsonSchema, value, onError) {
|
|
89
|
+
if (zodSchema !== void 0 && isObject(zodSchema)) {
|
|
90
|
+
const safeParseFn = zodSchema.safeParse;
|
|
91
|
+
if (isCallable(safeParseFn)) {
|
|
92
|
+
const result = safeParseFn(value);
|
|
93
|
+
if (isObject(result) && "success" in result && result.success !== true) {
|
|
94
|
+
onError?.(result.error);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const parsed = z.fromJSONSchema(jsonSchema);
|
|
101
|
+
if (isObject(parsed)) {
|
|
102
|
+
const safeParseFn = parsed.safeParse;
|
|
103
|
+
if (isCallable(safeParseFn)) {
|
|
104
|
+
const result = safeParseFn(value);
|
|
105
|
+
if (isObject(result) && "success" in result && result.success !== true) onError?.(result.error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function renderField(tree, value, onChange, userResolver, renderChild) {
|
|
110
|
+
const componentHint = tree.meta.component;
|
|
111
|
+
if (typeof componentHint === "string") {
|
|
112
|
+
const widget = widgetRegistry.get(componentHint);
|
|
113
|
+
if (widget !== void 0) {
|
|
114
|
+
const result = widget(buildRenderProps(tree, value, onChange, renderChild));
|
|
115
|
+
if (result !== void 0 && result !== null) {
|
|
116
|
+
if (isValidElement(result)) return result;
|
|
117
|
+
if (typeof result === "string" || typeof result === "number") return result;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const resolver = userResolver !== void 0 ? mergeResolvers(userResolver, headlessResolver) : headlessResolver;
|
|
123
|
+
const renderFn = getRenderFunction(tree.type, resolver);
|
|
124
|
+
if (renderFn !== void 0) {
|
|
125
|
+
let result;
|
|
126
|
+
try {
|
|
127
|
+
result = renderFn(buildRenderProps(tree, value, onChange, renderChild));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new SchemaRenderError(err instanceof Error ? err.message : `Render function threw for type "${tree.type}"`, tree, tree.type, err);
|
|
130
|
+
}
|
|
131
|
+
if (result !== void 0 && result !== null) {
|
|
132
|
+
if (isValidElement(result)) return result;
|
|
133
|
+
if (typeof result === "string" || typeof result === "number") return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (value === void 0 || value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
137
|
+
return /* @__PURE__ */ jsx("span", { children: typeof value === "string" ? value : JSON.stringify(value) });
|
|
138
|
+
}
|
|
139
|
+
function buildRenderProps(tree, value, onChange, renderChild) {
|
|
140
|
+
const props = {
|
|
141
|
+
value,
|
|
142
|
+
onChange,
|
|
143
|
+
readOnly: tree.editability === "presentation",
|
|
144
|
+
writeOnly: tree.editability === "input",
|
|
145
|
+
meta: tree.meta,
|
|
146
|
+
constraints: tree.constraints,
|
|
147
|
+
path: "",
|
|
148
|
+
tree,
|
|
149
|
+
renderChild
|
|
150
|
+
};
|
|
151
|
+
if (tree.enumValues !== void 0) props.enumValues = tree.enumValues;
|
|
152
|
+
if (tree.element !== void 0) props.element = tree.element;
|
|
153
|
+
if (tree.fields !== void 0) props.fields = tree.fields;
|
|
154
|
+
if (tree.options !== void 0) props.options = tree.options;
|
|
155
|
+
if (tree.discriminator !== void 0) props.discriminator = tree.discriminator;
|
|
156
|
+
if (tree.keyType !== void 0) props.keyType = tree.keyType;
|
|
157
|
+
if (tree.valueType !== void 0) props.valueType = tree.valueType;
|
|
158
|
+
return props;
|
|
159
|
+
}
|
|
160
|
+
function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
|
|
161
|
+
const userResolver = useContext(UserResolverContext);
|
|
162
|
+
let jsonSchema;
|
|
163
|
+
let zodSchema;
|
|
164
|
+
let rootMeta;
|
|
165
|
+
let rootDocument;
|
|
166
|
+
try {
|
|
167
|
+
const normalised = normaliseSchema(schemaInput, refInput);
|
|
168
|
+
jsonSchema = normalised.jsonSchema;
|
|
169
|
+
zodSchema = normalised.zodSchema;
|
|
170
|
+
rootMeta = normalised.rootMeta;
|
|
171
|
+
rootDocument = normalised.rootDocument;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw new SchemaNormalisationError(err instanceof Error ? err.message : "Failed to normalise schema", schemaInput, detectNormalisationKind(err));
|
|
174
|
+
}
|
|
175
|
+
const fieldTree = resolvePath(walk(jsonSchema, {
|
|
176
|
+
componentMeta: fieldMeta,
|
|
177
|
+
rootMeta,
|
|
178
|
+
rootDocument
|
|
179
|
+
}), path);
|
|
180
|
+
if (fieldTree === void 0) throw new SchemaFieldError(`Field not found: ${path}`, schemaInput, path);
|
|
181
|
+
const fieldValue = resolveValue(value, path);
|
|
182
|
+
const handleChange = useCallback((nextFieldValue) => {
|
|
183
|
+
if (validate) {
|
|
184
|
+
const newRootValue = setNestedValue(value, path, nextFieldValue);
|
|
185
|
+
runValidation(zodSchema, jsonSchema, newRootValue, onValidationError);
|
|
186
|
+
}
|
|
187
|
+
const newRootValue = setNestedValue(value, path, nextFieldValue);
|
|
188
|
+
onChange?.(newRootValue);
|
|
189
|
+
}, [
|
|
190
|
+
validate,
|
|
191
|
+
zodSchema,
|
|
192
|
+
jsonSchema,
|
|
193
|
+
value,
|
|
194
|
+
path,
|
|
195
|
+
onChange,
|
|
196
|
+
onValidationError
|
|
197
|
+
]);
|
|
198
|
+
const renderChild = (childTree, childValue, childOnChange) => {
|
|
199
|
+
return renderField(childTree, childValue, childOnChange, userResolver, renderChild);
|
|
200
|
+
};
|
|
201
|
+
return renderField(fieldTree, fieldValue, handleChange, userResolver, renderChild);
|
|
202
|
+
}
|
|
203
|
+
function resolvePath(tree, path) {
|
|
204
|
+
if (path.length === 0) return tree;
|
|
205
|
+
const parts = path.split(".");
|
|
206
|
+
let current = tree;
|
|
207
|
+
for (const part of parts) {
|
|
208
|
+
if (current === void 0) return void 0;
|
|
209
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
210
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
211
|
+
const arrayField = bracketMatch[1];
|
|
212
|
+
if (current.fields !== void 0) current = current.fields[arrayField];
|
|
213
|
+
if (current?.element !== void 0) current = current.element;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (current.fields !== void 0) current = current.fields[part];
|
|
217
|
+
else if (current.element !== void 0) current = current.element;
|
|
218
|
+
else return;
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
function resolveValue(root, path) {
|
|
223
|
+
if (path.length === 0) return root;
|
|
224
|
+
const parts = path.split(".");
|
|
225
|
+
let current = root;
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
228
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
229
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
230
|
+
const key = bracketMatch[1];
|
|
231
|
+
const index = Number(bracketMatch[2]);
|
|
232
|
+
const arr = toRecord(current)[key];
|
|
233
|
+
if (Array.isArray(arr)) current = arr[index];
|
|
234
|
+
else return;
|
|
235
|
+
} else current = toRecord(current)[part];
|
|
236
|
+
}
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
function setNestedValue(root, path, leafValue) {
|
|
240
|
+
if (path.length === 0) return leafValue;
|
|
241
|
+
const parts = path.split(".");
|
|
242
|
+
const result = isObject(root) ? { ...toRecord(root) } : {};
|
|
243
|
+
let current = result;
|
|
244
|
+
for (let i = 0; i < parts.length; i++) {
|
|
245
|
+
const part = parts[i];
|
|
246
|
+
if (part === void 0) break;
|
|
247
|
+
const isLast = i === parts.length - 1;
|
|
248
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
249
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
250
|
+
const key = bracketMatch[1];
|
|
251
|
+
const index = Number(bracketMatch[2]);
|
|
252
|
+
const existing = current[key];
|
|
253
|
+
const arr = Array.isArray(existing) ? existing.slice() : [];
|
|
254
|
+
if (isLast) arr[index] = leafValue;
|
|
255
|
+
current[key] = arr;
|
|
256
|
+
const nextCurrent = arr[index];
|
|
257
|
+
if (nextCurrent !== void 0 && isObject(nextCurrent)) current = toRecord(nextCurrent);
|
|
258
|
+
} else if (isLast) current[part] = leafValue;
|
|
259
|
+
else {
|
|
260
|
+
const existing = current[part];
|
|
261
|
+
const next = isObject(existing) ? { ...toRecord(existing) } : {};
|
|
262
|
+
current[part] = next;
|
|
263
|
+
current = next;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function isCallable(value) {
|
|
269
|
+
return typeof value === "function";
|
|
270
|
+
}
|
|
271
|
+
function detectNormalisationKind(err) {
|
|
272
|
+
if (err instanceof Error) {
|
|
273
|
+
const msg = err.message;
|
|
274
|
+
if (msg.includes("Zod 3")) return "zod3-unsupported";
|
|
275
|
+
if (msg.includes("Invalid Zod 4")) return "invalid-zod";
|
|
276
|
+
if (msg.includes("OpenAPI ref not found")) return "openapi-missing-ref";
|
|
277
|
+
if (msg.includes("OpenAPI")) return "openapi-invalid";
|
|
278
|
+
if (msg.includes("JSON Schema")) return "invalid-json-schema";
|
|
279
|
+
}
|
|
280
|
+
return "unknown";
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { SchemaComponent, SchemaField, SchemaProvider, registerWidget, renderField };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Component, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react/SchemaErrorBoundary.d.ts
|
|
4
|
+
interface SchemaErrorBoundaryProps {
|
|
5
|
+
/** Called with the caught error. Returns fallback ReactNode to render. */
|
|
6
|
+
fallback: (error: Error, reset: () => void) => ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
interface ErrorBoundaryState {
|
|
10
|
+
error: Error | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* React error boundary that catches schema rendering errors.
|
|
14
|
+
*
|
|
15
|
+
* Provides a `reset` callback that clears the error state, allowing
|
|
16
|
+
* the children to re-render (e.g. after fixing a bad schema prop).
|
|
17
|
+
*/
|
|
18
|
+
declare class SchemaErrorBoundary extends Component<SchemaErrorBoundaryProps, ErrorBoundaryState> {
|
|
19
|
+
state: ErrorBoundaryState;
|
|
20
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
21
|
+
componentDidCatch(error: Error): void;
|
|
22
|
+
reset: () => void;
|
|
23
|
+
render(): ReactNode;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { SchemaErrorBoundary, SchemaErrorBoundaryProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SchemaError } from "../core/errors.mjs";
|
|
2
|
+
import { Component } from "react";
|
|
3
|
+
//#region src/react/SchemaErrorBoundary.tsx
|
|
4
|
+
/**
|
|
5
|
+
* React error boundary for schema-components.
|
|
6
|
+
*
|
|
7
|
+
* Catches render errors from `<SchemaComponent>`, theme adapters, and
|
|
8
|
+
* any child components. Without this boundary, a throwing render function
|
|
9
|
+
* crashes the entire React tree.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";
|
|
13
|
+
*
|
|
14
|
+
* <SchemaErrorBoundary fallback={(error) => <p>{error.message}</p>}>
|
|
15
|
+
* <SchemaComponent schema={userSchema} value={user} />
|
|
16
|
+
* </SchemaErrorBoundary>
|
|
17
|
+
*
|
|
18
|
+
* The boundary catches `SchemaRenderError` from theme adapters and any
|
|
19
|
+
* other errors thrown during rendering. It does NOT catch:
|
|
20
|
+
* - Event handler errors (onChange, etc.)
|
|
21
|
+
* - Async errors
|
|
22
|
+
* - Errors in server-side rendering
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* React error boundary that catches schema rendering errors.
|
|
26
|
+
*
|
|
27
|
+
* Provides a `reset` callback that clears the error state, allowing
|
|
28
|
+
* the children to re-render (e.g. after fixing a bad schema prop).
|
|
29
|
+
*/
|
|
30
|
+
var SchemaErrorBoundary = class extends Component {
|
|
31
|
+
state = { error: void 0 };
|
|
32
|
+
static getDerivedStateFromError(error) {
|
|
33
|
+
return { error };
|
|
34
|
+
}
|
|
35
|
+
componentDidCatch(error) {
|
|
36
|
+
if (!(error instanceof SchemaError)) console.error("[schema-components] Unhandled render error:", error);
|
|
37
|
+
}
|
|
38
|
+
reset = () => {
|
|
39
|
+
this.setState({ error: void 0 });
|
|
40
|
+
};
|
|
41
|
+
render() {
|
|
42
|
+
if (this.state.error !== void 0) return this.props.fallback(this.state.error, this.reset);
|
|
43
|
+
return this.props.children;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { SchemaErrorBoundary };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { b as ComponentResolver } from "../types-BU0ETFHk.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/headless.d.ts
|
|
5
|
+
declare function toReactNode(value: unknown): ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* The headless resolver uses props.renderChild for recursive rendering.
|
|
8
|
+
* No factory function needed — the renderChild is always available
|
|
9
|
+
* on RenderProps.
|
|
10
|
+
*/
|
|
11
|
+
declare const headlessResolver: ComponentResolver;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { headlessResolver, toReactNode };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { isValidElement } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/headless.tsx
|
|
5
|
+
/**
|
|
6
|
+
* React headless renderer — the default ComponentResolver implementation.
|
|
7
|
+
*
|
|
8
|
+
* Produces plain HTML elements for every schema type. Theme adapters
|
|
9
|
+
* replace this by implementing ComponentResolver with their own components.
|
|
10
|
+
*
|
|
11
|
+
* This module imports React and lives in the react layer, not core,
|
|
12
|
+
* because it produces ReactNode values.
|
|
13
|
+
*/
|
|
14
|
+
function toReactNode(value) {
|
|
15
|
+
if (value === null || value === void 0) return null;
|
|
16
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
17
|
+
if (isValidElement(value)) return value;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function renderString(props) {
|
|
21
|
+
if (props.readOnly) {
|
|
22
|
+
const strValue = typeof props.value === "string" ? props.value : void 0;
|
|
23
|
+
if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
24
|
+
const format = props.constraints.format;
|
|
25
|
+
if (format === "email") return /* @__PURE__ */ jsx("a", {
|
|
26
|
+
href: `mailto:${strValue}`,
|
|
27
|
+
children: strValue
|
|
28
|
+
});
|
|
29
|
+
if (format === "uri" || format === "url") return /* @__PURE__ */ jsx("a", {
|
|
30
|
+
href: strValue,
|
|
31
|
+
children: strValue
|
|
32
|
+
});
|
|
33
|
+
return /* @__PURE__ */ jsx("span", { children: strValue });
|
|
34
|
+
}
|
|
35
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
36
|
+
if (props.enumValues !== void 0 && props.enumValues.length > 0) return /* @__PURE__ */ jsxs("select", {
|
|
37
|
+
value: strValue,
|
|
38
|
+
onChange: (e) => {
|
|
39
|
+
props.onChange(e.target.value);
|
|
40
|
+
},
|
|
41
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
42
|
+
value: "",
|
|
43
|
+
children: "Select…"
|
|
44
|
+
}), props.enumValues.map((v) => /* @__PURE__ */ jsx("option", {
|
|
45
|
+
value: v,
|
|
46
|
+
children: v
|
|
47
|
+
}, v))]
|
|
48
|
+
});
|
|
49
|
+
return /* @__PURE__ */ jsx("input", {
|
|
50
|
+
type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
|
|
51
|
+
value: props.writeOnly ? "" : strValue,
|
|
52
|
+
onChange: (e) => {
|
|
53
|
+
props.onChange(e.target.value);
|
|
54
|
+
},
|
|
55
|
+
placeholder: typeof props.meta.description === "string" ? props.meta.description : void 0,
|
|
56
|
+
minLength: props.constraints.minLength,
|
|
57
|
+
maxLength: props.constraints.maxLength
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function renderNumber(props) {
|
|
61
|
+
if (props.readOnly) {
|
|
62
|
+
if (typeof props.value !== "number") return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
63
|
+
return /* @__PURE__ */ jsx("span", { children: props.value.toLocaleString() });
|
|
64
|
+
}
|
|
65
|
+
const numValue = typeof props.value === "number" ? props.value : "";
|
|
66
|
+
return /* @__PURE__ */ jsx("input", {
|
|
67
|
+
type: "number",
|
|
68
|
+
value: props.writeOnly ? "" : numValue,
|
|
69
|
+
onChange: (e) => {
|
|
70
|
+
props.onChange(Number(e.target.value));
|
|
71
|
+
},
|
|
72
|
+
min: props.constraints.minimum,
|
|
73
|
+
max: props.constraints.maximum
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function renderBoolean(props) {
|
|
77
|
+
if (props.readOnly) {
|
|
78
|
+
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
79
|
+
return /* @__PURE__ */ jsx("span", { children: props.value ? "Yes" : "No" });
|
|
80
|
+
}
|
|
81
|
+
return /* @__PURE__ */ jsx("input", {
|
|
82
|
+
type: "checkbox",
|
|
83
|
+
checked: props.value === true,
|
|
84
|
+
onChange: (e) => {
|
|
85
|
+
props.onChange(e.target.checked);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function renderEnum(props) {
|
|
90
|
+
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
91
|
+
if (props.readOnly) return /* @__PURE__ */ jsx("span", { children: enumValue || "—" });
|
|
92
|
+
return /* @__PURE__ */ jsxs("select", {
|
|
93
|
+
value: props.writeOnly ? "" : enumValue,
|
|
94
|
+
onChange: (e) => {
|
|
95
|
+
props.onChange(e.target.value);
|
|
96
|
+
},
|
|
97
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
98
|
+
value: "",
|
|
99
|
+
children: "Select…"
|
|
100
|
+
}), props.enumValues?.map((v) => /* @__PURE__ */ jsx("option", {
|
|
101
|
+
value: v,
|
|
102
|
+
children: v
|
|
103
|
+
}, v))]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function renderObject(props) {
|
|
107
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
108
|
+
const fields = props.fields;
|
|
109
|
+
if (fields === void 0) return null;
|
|
110
|
+
return /* @__PURE__ */ jsxs("fieldset", { children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx("legend", { children: props.meta.description }), Object.entries(fields).map(([key, field]) => {
|
|
111
|
+
const childValue = obj[key];
|
|
112
|
+
const childOnChange = (v) => {
|
|
113
|
+
const updated = {};
|
|
114
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
115
|
+
updated[key] = v;
|
|
116
|
+
props.onChange(updated);
|
|
117
|
+
};
|
|
118
|
+
return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsx("label", { children: field.meta.description }), toReactNode(props.renderChild(field, childValue, childOnChange))] }, key);
|
|
119
|
+
})] });
|
|
120
|
+
}
|
|
121
|
+
function renderArray(props) {
|
|
122
|
+
const arr = Array.isArray(props.value) ? props.value : [];
|
|
123
|
+
const element = props.element;
|
|
124
|
+
if (element === void 0) return null;
|
|
125
|
+
return /* @__PURE__ */ jsx("div", { children: arr.map((item, i) => {
|
|
126
|
+
const childOnChange = (v) => {
|
|
127
|
+
const next = arr.slice();
|
|
128
|
+
next[i] = v;
|
|
129
|
+
props.onChange(next);
|
|
130
|
+
};
|
|
131
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
132
|
+
}) });
|
|
133
|
+
}
|
|
134
|
+
function renderUnknown(props) {
|
|
135
|
+
if (props.readOnly) {
|
|
136
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
137
|
+
return /* @__PURE__ */ jsx("span", { children: typeof props.value === "string" ? props.value : JSON.stringify(props.value) });
|
|
138
|
+
}
|
|
139
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
140
|
+
return /* @__PURE__ */ jsx("input", {
|
|
141
|
+
type: "text",
|
|
142
|
+
value: props.writeOnly ? "" : strValue,
|
|
143
|
+
onChange: (e) => {
|
|
144
|
+
props.onChange(e.target.value);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The headless resolver uses props.renderChild for recursive rendering.
|
|
150
|
+
* No factory function needed — the renderChild is always available
|
|
151
|
+
* on RenderProps.
|
|
152
|
+
*/
|
|
153
|
+
const headlessResolver = {
|
|
154
|
+
string: renderString,
|
|
155
|
+
number: renderNumber,
|
|
156
|
+
boolean: renderBoolean,
|
|
157
|
+
enum: renderEnum,
|
|
158
|
+
object: renderObject,
|
|
159
|
+
array: renderArray,
|
|
160
|
+
unknown: renderUnknown
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
export { headlessResolver, toReactNode };
|