schema-components 1.16.3 → 1.18.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/dist/core/adapter.d.mts +1 -1
- package/dist/core/constraints.d.mts +1 -1
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/merge.d.mts +14 -8
- package/dist/core/merge.mjs +81 -12
- package/dist/core/normalise.d.mts +1 -1
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/renderer.d.mts +2 -2
- package/dist/core/renderer.mjs +50 -1
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/typeInference.d.mts +2 -2
- package/dist/core/walkBuilders.d.mts +2 -2
- package/dist/core/walker.mjs +2 -2
- package/dist/{diagnostics-DzbZmcLI.d.mts → diagnostics-BYk63jsC.d.mts} +1 -1
- package/dist/html/a11y.d.mts +13 -2
- package/dist/html/a11y.mjs +26 -2
- package/dist/html/renderToHtml.d.mts +1 -1
- package/dist/html/renderToHtml.mjs +5 -3
- package/dist/html/renderToHtmlStream.d.mts +1 -1
- package/dist/html/renderers.d.mts +4 -3
- package/dist/html/renderers.mjs +9 -13
- package/dist/html/streamRenderers.d.mts +1 -1
- package/dist/openapi/bundle.d.mts +9 -4
- package/dist/openapi/bundle.mjs +73 -15
- package/dist/openapi/components.d.mts +1 -1
- package/dist/openapi/components.mjs +61 -27
- package/dist/openapi/parser.mjs +8 -8
- package/dist/openapi/resolve.d.mts +13 -2
- package/dist/openapi/resolve.mjs +19 -3
- package/dist/react/SchemaComponent.d.mts +35 -7
- package/dist/react/SchemaComponent.mjs +49 -43
- package/dist/react/SchemaView.d.mts +12 -4
- package/dist/react/SchemaView.mjs +24 -49
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +15 -2
- package/dist/react/headlessRenderers.mjs +52 -37
- package/dist/{ref-DvWoULcy.d.mts → ref-Ckt5liZs.d.mts} +1 -1
- package/dist/{renderer-BdSqllx5.d.mts → renderer-DXo-rXHJ.d.mts} +28 -2
- package/dist/themes/mantine.d.mts +6 -1
- package/dist/themes/mantine.mjs +44 -11
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/mui.mjs +23 -8
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/radix.mjs +43 -11
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +28 -10
- package/dist/{typeInference-k7FXfTVO.d.mts → typeInference-5JiqIZ8t.d.mts} +57 -4
- package/package.json +5 -2
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { isObject, toRecordOrUndefined } from "../core/guards.mjs";
|
|
3
3
|
import { SchemaFieldError, SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
4
4
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
5
|
-
import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
5
|
+
import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
6
6
|
import { walk } from "../core/walker.mjs";
|
|
7
7
|
import { headlessResolver } from "./headless.mjs";
|
|
8
8
|
import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { createContext, isValidElement, useCallback, useContext, useId, useMemo } from "react";
|
|
10
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
|
|
12
12
|
//#region src/react/SchemaComponent.tsx
|
|
13
13
|
/**
|
|
14
14
|
* <SchemaComponent> — renders UI from Zod, JSON Schema, or OpenAPI schemas.
|
|
@@ -46,9 +46,11 @@ const globalWidgets = /* @__PURE__ */ new Map();
|
|
|
46
46
|
function registerWidget(name, render) {
|
|
47
47
|
globalWidgets.set(name, render);
|
|
48
48
|
}
|
|
49
|
-
function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, onDiagnostic, strict, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets }) {
|
|
49
|
+
function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange, validate, onValidationError, onError, onDiagnostic, strict, fields, meta: componentMeta, readOnly, writeOnly, description, widgets: instanceWidgets, idPrefix }) {
|
|
50
50
|
const userResolver = useContext(UserResolverContext);
|
|
51
51
|
const contextWidgets = useContext(WidgetsContext);
|
|
52
|
+
const generatedId = useId();
|
|
53
|
+
const rootPath = idPrefix ?? sanitisePrefix(generatedId);
|
|
52
54
|
const mergedMeta = useMemo(() => {
|
|
53
55
|
const merged = { ...componentMeta };
|
|
54
56
|
if (readOnly === true) merged.readOnly = true;
|
|
@@ -108,11 +110,40 @@ function SchemaComponent({ schema: schemaInput, ref: refInput, value, onChange,
|
|
|
108
110
|
...diagnostics !== void 0 ? { diagnostics } : {}
|
|
109
111
|
};
|
|
110
112
|
const tree = walk(jsonSchema, walkOptions);
|
|
111
|
-
const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
|
|
112
|
-
|
|
113
|
+
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
|
|
114
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
115
|
+
return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, instanceWidgets, contextWidgets, currentDepth + 1);
|
|
113
116
|
};
|
|
114
|
-
const renderChild = makeRenderChild(0);
|
|
115
|
-
return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, instanceWidgets, contextWidgets, 0);
|
|
117
|
+
const renderChild = makeRenderChild(0, rootPath);
|
|
118
|
+
return renderField(tree, value ?? tree.defaultValue, handleChange, userResolver, renderChild, rootPath, instanceWidgets, contextWidgets, 0);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Default root-path sentinel used when no `idPrefix` is supplied AND the
|
|
122
|
+
* component is rendered outside a React tree (e.g. server-side bundling
|
|
123
|
+
* test harnesses). Production callers receive a `useId()`-derived prefix
|
|
124
|
+
* that is unique per instance.
|
|
125
|
+
*/
|
|
126
|
+
const ROOT_PATH = "root";
|
|
127
|
+
/**
|
|
128
|
+
* Append a child path suffix to a parent path. When the suffix is omitted
|
|
129
|
+
* (e.g. transparent wrappers like union options), the parent path is
|
|
130
|
+
* returned unchanged so the child inherits the parent's id.
|
|
131
|
+
*/
|
|
132
|
+
function joinPath(parent, suffix) {
|
|
133
|
+
if (suffix === void 0 || suffix.length === 0) return parent;
|
|
134
|
+
if (parent.length === 0) return suffix;
|
|
135
|
+
return `${parent}.${suffix}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Normalise a `useId()` value into a DOM-id-safe prefix. React's `useId`
|
|
139
|
+
* returns values containing `:` characters (e.g. `«:r0:»`) which are
|
|
140
|
+
* invalid in CSS selectors. Replace any run of non-alphanumeric characters
|
|
141
|
+
* with a single hyphen and trim leading/trailing hyphens.
|
|
142
|
+
*/
|
|
143
|
+
function sanitisePrefix(value) {
|
|
144
|
+
const sanitised = value.replace(/[^a-zA-Z0-9_]+/g, "-").replace(/^-+|-+$/g, "");
|
|
145
|
+
if (sanitised.length === 0) throw new Error(`Cannot derive a DOM-safe id prefix from "${value}". Pass an explicit idPrefix prop.`);
|
|
146
|
+
return sanitised;
|
|
116
147
|
}
|
|
117
148
|
function runValidation(zodSchema, jsonSchema, value) {
|
|
118
149
|
if (zodSchema !== void 0 && isObject(zodSchema)) {
|
|
@@ -134,7 +165,8 @@ function runValidation(zodSchema, jsonSchema, value) {
|
|
|
134
165
|
}
|
|
135
166
|
/** Maximum rendering depth before treating a field as recursive. */
|
|
136
167
|
const MAX_RENDER_DEPTH = 10;
|
|
137
|
-
function renderField(tree, value, onChange, userResolver, renderChild, instanceWidgets, contextWidgets, depth = 0) {
|
|
168
|
+
function renderField(tree, value, onChange, userResolver, renderChild, path, instanceWidgets, contextWidgets, depth = 0) {
|
|
169
|
+
if (path.length === 0) throw new Error("renderField requires a non-empty path. Pass ROOT_PATH for the root field and use renderChild's pathSuffix to derive child paths.");
|
|
138
170
|
if (depth >= MAX_RENDER_DEPTH) {
|
|
139
171
|
const refTarget = tree.type === "recursive" ? tree.refTarget : "";
|
|
140
172
|
return /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsxs("em", { children: [
|
|
@@ -147,7 +179,7 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
|
|
|
147
179
|
if (typeof componentHint === "string") {
|
|
148
180
|
const widget = instanceWidgets?.get(componentHint) ?? contextWidgets?.get(componentHint) ?? globalWidgets.get(componentHint);
|
|
149
181
|
if (widget !== void 0) {
|
|
150
|
-
const result = widget(buildRenderProps(tree, value, onChange, renderChild));
|
|
182
|
+
const result = widget(buildRenderProps(tree, value, onChange, renderChild, path));
|
|
151
183
|
if (result !== void 0 && result !== null) {
|
|
152
184
|
if (isValidElement(result)) return result;
|
|
153
185
|
if (typeof result === "string" || typeof result === "number") return result;
|
|
@@ -160,7 +192,7 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
|
|
|
160
192
|
if (renderFn !== void 0) {
|
|
161
193
|
let result;
|
|
162
194
|
try {
|
|
163
|
-
result = renderFn(buildRenderProps(tree, value, onChange, renderChild));
|
|
195
|
+
result = renderFn(buildRenderProps(tree, value, onChange, renderChild, path));
|
|
164
196
|
} catch (err) {
|
|
165
197
|
throw new SchemaRenderError(err instanceof Error ? err.message : `Render function threw for type "${tree.type}"`, tree, tree.type, err);
|
|
166
198
|
}
|
|
@@ -171,38 +203,10 @@ function renderField(tree, value, onChange, userResolver, renderChild, instanceW
|
|
|
171
203
|
if (value === void 0 || value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
172
204
|
return /* @__PURE__ */ jsx("span", { children: typeof value === "string" ? value : JSON.stringify(value) });
|
|
173
205
|
}
|
|
174
|
-
function buildRenderProps(tree, value, onChange, renderChild) {
|
|
175
|
-
const props = {
|
|
176
|
-
value,
|
|
177
|
-
onChange,
|
|
178
|
-
readOnly: tree.editability === "presentation",
|
|
179
|
-
writeOnly: tree.editability === "input",
|
|
180
|
-
meta: tree.meta,
|
|
181
|
-
constraints: tree.constraints,
|
|
182
|
-
path: "",
|
|
183
|
-
tree,
|
|
184
|
-
renderChild
|
|
185
|
-
};
|
|
186
|
-
if (tree.type === "enum") props.enumValues = tree.enumValues;
|
|
187
|
-
if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
|
|
188
|
-
if (tree.type === "object") props.fields = tree.fields;
|
|
189
|
-
if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
|
|
190
|
-
if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
|
|
191
|
-
if (tree.type === "record") props.keyType = tree.keyType;
|
|
192
|
-
if (tree.type === "record") props.valueType = tree.valueType;
|
|
193
|
-
if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
|
|
194
|
-
if (tree.type === "conditional") props.ifClause = tree.ifClause;
|
|
195
|
-
if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
|
|
196
|
-
if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
|
|
197
|
-
if (tree.type === "negation") props.negated = tree.negated;
|
|
198
|
-
if (tree.type === "recursive") props.refTarget = tree.refTarget;
|
|
199
|
-
if (tree.type === "literal") props.literalValues = tree.literalValues;
|
|
200
|
-
if (tree.examples !== void 0) props.examples = tree.examples;
|
|
201
|
-
return props;
|
|
202
|
-
}
|
|
203
206
|
function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
|
|
204
207
|
const userResolver = useContext(UserResolverContext);
|
|
205
208
|
const contextWidgets = useContext(WidgetsContext);
|
|
209
|
+
const generatedId = useId();
|
|
206
210
|
let jsonSchema;
|
|
207
211
|
let zodSchema;
|
|
208
212
|
let rootMeta;
|
|
@@ -240,10 +244,12 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
|
|
|
240
244
|
onChange,
|
|
241
245
|
onValidationError
|
|
242
246
|
]);
|
|
243
|
-
const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
|
|
244
|
-
|
|
247
|
+
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
|
|
248
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
249
|
+
return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, void 0, contextWidgets, currentDepth + 1);
|
|
245
250
|
};
|
|
246
|
-
|
|
251
|
+
const rootPath = joinPath(sanitisePrefix(generatedId), path);
|
|
252
|
+
return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0, rootPath), rootPath, void 0, contextWidgets, 0);
|
|
247
253
|
}
|
|
248
254
|
/**
|
|
249
255
|
* Dispatch Zod errors to per-field onValidationError callbacks.
|
|
@@ -290,4 +296,4 @@ function detectNormalisationKind(err) {
|
|
|
290
296
|
return "unknown";
|
|
291
297
|
}
|
|
292
298
|
//#endregion
|
|
293
|
-
export { SchemaComponent, SchemaField, SchemaProvider, registerWidget, renderField };
|
|
299
|
+
export { ROOT_PATH, SchemaComponent, SchemaField, SchemaProvider, joinPath, registerWidget, renderField, sanitisePrefix };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { T as SchemaMeta } from "../types-D_5ST7SS.mjs";
|
|
2
|
-
import { t as Diagnostic } from "../diagnostics-
|
|
3
|
-
import { r as ComponentResolver } from "../renderer-
|
|
2
|
+
import { t as Diagnostic } from "../diagnostics-BYk63jsC.mjs";
|
|
3
|
+
import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
|
|
4
4
|
import { WidgetMap } from "./SchemaComponent.mjs";
|
|
5
5
|
import { ReactNode } from "react";
|
|
6
6
|
|
|
@@ -30,9 +30,16 @@ interface SchemaViewProps {
|
|
|
30
30
|
onDiagnostic?: (diagnostic: Diagnostic) => void;
|
|
31
31
|
/** When true, any diagnostic becomes a thrown error. */
|
|
32
32
|
strict?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Prefix used for every input `id`/label `htmlFor` in this view subtree.
|
|
35
|
+
* Defaults to a per-instance value from `useId()`; pass a deterministic
|
|
36
|
+
* value when stable ids matter (e.g. snapshot tests).
|
|
37
|
+
*/
|
|
38
|
+
idPrefix?: string;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
|
-
* Server-safe schema renderer — no
|
|
41
|
+
* Server-safe schema renderer — no context and no state. The only hook
|
|
42
|
+
* called is `useId()`, which is RSC-safe.
|
|
36
43
|
*
|
|
37
44
|
* Always renders in read-only mode. For editable forms, use
|
|
38
45
|
* `<SchemaComponent>` with `"use client"`.
|
|
@@ -47,7 +54,8 @@ declare function SchemaView({
|
|
|
47
54
|
resolver,
|
|
48
55
|
widgets,
|
|
49
56
|
onDiagnostic,
|
|
50
|
-
strict
|
|
57
|
+
strict,
|
|
58
|
+
idPrefix
|
|
51
59
|
}: SchemaViewProps): ReactNode;
|
|
52
60
|
//#endregion
|
|
53
61
|
export { SchemaView, SchemaViewProps };
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
|
|
2
2
|
import { normaliseSchema } from "../core/adapter.mjs";
|
|
3
|
-
import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
3
|
+
import { buildRenderProps, getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
|
|
4
4
|
import { walk } from "../core/walker.mjs";
|
|
5
5
|
import { headlessResolver } from "./headless.mjs";
|
|
6
|
+
import { joinPath, sanitisePrefix } from "./SchemaComponent.mjs";
|
|
7
|
+
import { createElement, isValidElement, useId } from "react";
|
|
6
8
|
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import { createElement, isValidElement } from "react";
|
|
8
9
|
//#region src/react/SchemaView.tsx
|
|
9
10
|
/**
|
|
10
11
|
* React Server Component for read-only schema rendering.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Uses no React state, context, or effects — no `useContext`, `useMemo`,
|
|
14
|
+
* `useCallback`, `useState`, or `useEffect`. The single hook called is
|
|
15
|
+
* `useId()`, which is one of the few hooks permitted inside a React
|
|
16
|
+
* Server Component (it is RSC-safe by design) and is used solely to
|
|
17
|
+
* derive a stable per-instance `idPrefix`. The component therefore runs
|
|
18
|
+
* in an RSC environment without the `"use client"` directive.
|
|
15
19
|
*
|
|
16
20
|
* **Read-only only.** For interactive forms with `onChange`, use
|
|
17
21
|
* `<SchemaComponent>` (which requires `"use client"`).
|
|
@@ -30,14 +34,16 @@ import { createElement, isValidElement } from "react";
|
|
|
30
34
|
* Server Components cannot use React context, so the resolver
|
|
31
35
|
* is passed explicitly.
|
|
32
36
|
*/
|
|
33
|
-
function noop() {}
|
|
34
37
|
/**
|
|
35
|
-
* Server-safe schema renderer — no
|
|
38
|
+
* Server-safe schema renderer — no context and no state. The only hook
|
|
39
|
+
* called is `useId()`, which is RSC-safe.
|
|
36
40
|
*
|
|
37
41
|
* Always renders in read-only mode. For editable forms, use
|
|
38
42
|
* `<SchemaComponent>` with `"use client"`.
|
|
39
43
|
*/
|
|
40
|
-
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict }) {
|
|
44
|
+
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict, idPrefix }) {
|
|
45
|
+
const generatedId = useId();
|
|
46
|
+
const rootPath = idPrefix ?? sanitisePrefix(generatedId);
|
|
41
47
|
const mergedMeta = {
|
|
42
48
|
...componentMeta,
|
|
43
49
|
readOnly: true
|
|
@@ -68,29 +74,22 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
|
|
|
68
74
|
const tree = walk(jsonSchema, walkOptions);
|
|
69
75
|
const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
|
|
70
76
|
const MAX_SERVER_DEPTH = 10;
|
|
71
|
-
const makeRenderChild = (currentDepth) => (childTree, childValue) => {
|
|
77
|
+
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, pathSuffix) => {
|
|
78
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
72
79
|
if (currentDepth >= MAX_SERVER_DEPTH) return createElement("fieldset", null, createElement("em", null, `\u21bb ${typeof childTree.meta.description === "string" ? childTree.meta.description : childTree.type === "recursive" ? childTree.refTarget : "schema"} (recursive)`));
|
|
73
|
-
return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1), widgets);
|
|
80
|
+
return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, widgets);
|
|
74
81
|
};
|
|
75
|
-
const renderChild = makeRenderChild(0);
|
|
76
|
-
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
|
|
82
|
+
const renderChild = makeRenderChild(0, rootPath);
|
|
83
|
+
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, rootPath, widgets);
|
|
77
84
|
}
|
|
78
|
-
function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
85
|
+
function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
|
|
86
|
+
if (path.length === 0) throw new Error("renderFieldServer requires a non-empty path. Pass ROOT_PATH at the root and join children via joinPath().");
|
|
87
|
+
const adaptedRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
|
|
79
88
|
const componentHint = tree.meta.component;
|
|
80
89
|
if (typeof componentHint === "string") {
|
|
81
90
|
const widget = widgets?.get(componentHint);
|
|
82
91
|
if (widget !== void 0) {
|
|
83
|
-
const result = widget(
|
|
84
|
-
value,
|
|
85
|
-
onChange: noop,
|
|
86
|
-
readOnly: true,
|
|
87
|
-
writeOnly: false,
|
|
88
|
-
meta: tree.meta,
|
|
89
|
-
constraints: tree.constraints,
|
|
90
|
-
path: "",
|
|
91
|
-
tree,
|
|
92
|
-
renderChild: (childTree, childValue) => renderChild(childTree, childValue)
|
|
93
|
-
});
|
|
92
|
+
const result = widget(buildRenderProps(tree, value, void 0, adaptedRenderChild, path));
|
|
94
93
|
if (result !== void 0 && result !== null) {
|
|
95
94
|
if (isValidElement(result)) return result;
|
|
96
95
|
if (typeof result === "string" || typeof result === "number") return result;
|
|
@@ -99,31 +98,7 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
|
99
98
|
}
|
|
100
99
|
const renderFn = getRenderFunction(tree.type, resolver);
|
|
101
100
|
if (renderFn !== void 0) {
|
|
102
|
-
const props =
|
|
103
|
-
value,
|
|
104
|
-
onChange: noop,
|
|
105
|
-
readOnly: true,
|
|
106
|
-
writeOnly: false,
|
|
107
|
-
meta: tree.meta,
|
|
108
|
-
constraints: tree.constraints,
|
|
109
|
-
path: "",
|
|
110
|
-
tree,
|
|
111
|
-
renderChild: (childTree, childValue) => renderChild(childTree, childValue)
|
|
112
|
-
};
|
|
113
|
-
if (tree.type === "enum") props.enumValues = tree.enumValues;
|
|
114
|
-
if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
|
|
115
|
-
if (tree.type === "object") props.fields = tree.fields;
|
|
116
|
-
if (tree.type === "union" || tree.type === "discriminatedUnion") props.options = tree.options;
|
|
117
|
-
if (tree.type === "discriminatedUnion") props.discriminator = tree.discriminator;
|
|
118
|
-
if (tree.type === "record") props.keyType = tree.keyType;
|
|
119
|
-
if (tree.type === "record") props.valueType = tree.valueType;
|
|
120
|
-
if (tree.type === "tuple") props.prefixItems = tree.prefixItems;
|
|
121
|
-
if (tree.type === "conditional") props.ifClause = tree.ifClause;
|
|
122
|
-
if (tree.type === "conditional" && tree.thenClause !== void 0) props.thenClause = tree.thenClause;
|
|
123
|
-
if (tree.type === "conditional" && tree.elseClause !== void 0) props.elseClause = tree.elseClause;
|
|
124
|
-
if (tree.type === "negation") props.negated = tree.negated;
|
|
125
|
-
if (tree.type === "recursive") props.refTarget = tree.refTarget;
|
|
126
|
-
if (tree.type === "literal") props.literalValues = tree.literalValues;
|
|
101
|
+
const props = buildRenderProps(tree, value, void 0, adaptedRenderChild, path);
|
|
127
102
|
try {
|
|
128
103
|
const result = renderFn(props);
|
|
129
104
|
if (result !== void 0 && result !== null) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as WalkedField } from "../types-D_5ST7SS.mjs";
|
|
2
|
-
import { l as RenderProps } from "../renderer-
|
|
2
|
+
import { l as RenderProps } from "../renderer-DXo-rXHJ.mjs";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react/headlessRenderers.d.ts
|
|
@@ -8,6 +8,19 @@ import { ReactNode } from "react";
|
|
|
8
8
|
* Returns `null` for unrecognised values.
|
|
9
9
|
*/
|
|
10
10
|
declare function toReactNode(value: unknown): ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Build a stable, unique input ID from the path.
|
|
13
|
+
* Used for `htmlFor`/`id` association between labels and inputs.
|
|
14
|
+
*
|
|
15
|
+
* Throws on an empty path: the previous "sc-field" fallback caused every
|
|
16
|
+
* input across a form to share the same id, breaking label-input pairing
|
|
17
|
+
* and screen reader navigation. Callers must thread a non-empty path
|
|
18
|
+
* (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
|
|
19
|
+
*
|
|
20
|
+
* Dots and bracket indices in paths are converted to hyphens to keep the
|
|
21
|
+
* id valid as a CSS selector and predictable in test queries.
|
|
22
|
+
*/
|
|
23
|
+
declare function inputId(path: string): string;
|
|
11
24
|
declare function renderString(props: RenderProps): ReactNode;
|
|
12
25
|
declare function renderNumber(props: RenderProps): ReactNode;
|
|
13
26
|
declare function renderBoolean(props: RenderProps): ReactNode;
|
|
@@ -46,4 +59,4 @@ declare function renderFile(props: RenderProps): ReactNode;
|
|
|
46
59
|
declare function renderRecursive(props: RenderProps): ReactNode;
|
|
47
60
|
declare function renderUnknown(props: RenderProps): ReactNode;
|
|
48
61
|
//#endregion
|
|
49
|
-
export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
62
|
+
export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { isValidElement, useCallback, useEffect, useRef } from "react";
|
|
2
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { isValidElement, useCallback, useRef } from "react";
|
|
4
4
|
//#region src/react/headlessRenderers.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Headless renderer functions — one per schema type.
|
|
@@ -57,12 +57,20 @@ function dateInputType(format) {
|
|
|
57
57
|
if (format === "date-time" || format === "datetime") return "datetime-local";
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
|
-
* Build a stable, unique
|
|
60
|
+
* Build a stable, unique input ID from the path.
|
|
61
61
|
* Used for `htmlFor`/`id` association between labels and inputs.
|
|
62
|
+
*
|
|
63
|
+
* Throws on an empty path: the previous "sc-field" fallback caused every
|
|
64
|
+
* input across a form to share the same id, breaking label-input pairing
|
|
65
|
+
* and screen reader navigation. Callers must thread a non-empty path
|
|
66
|
+
* (see `ROOT_PATH` and `joinPath` in `SchemaComponent.tsx`).
|
|
67
|
+
*
|
|
68
|
+
* Dots and bracket indices in paths are converted to hyphens to keep the
|
|
69
|
+
* id valid as a CSS selector and predictable in test queries.
|
|
62
70
|
*/
|
|
63
71
|
function inputId(path) {
|
|
64
|
-
if (path.length === 0)
|
|
65
|
-
return `sc-${path}`;
|
|
72
|
+
if (path.length === 0) throw new Error("inputId requires a non-empty path. Pass ROOT_PATH for the root field and use renderChild's pathSuffix to derive child paths.");
|
|
73
|
+
return `sc-${path.replace(/[.[\]]+/g, "-").replace(/-+$/g, "")}`;
|
|
66
74
|
}
|
|
67
75
|
function renderString(props) {
|
|
68
76
|
const id = inputId(props.path);
|
|
@@ -245,14 +253,14 @@ function renderObject(props) {
|
|
|
245
253
|
});
|
|
246
254
|
return /* @__PURE__ */ jsxs("fieldset", { children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx("legend", { children: props.meta.description }), sortedEntries.filter(([, field]) => field.meta.visible !== false).map(([key, field]) => {
|
|
247
255
|
const childValue = obj[key];
|
|
248
|
-
const childId = inputId(
|
|
256
|
+
const childId = inputId(`${props.path}.${key}`);
|
|
249
257
|
const childOnChange = (v) => {
|
|
250
258
|
const updated = {};
|
|
251
259
|
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
252
260
|
updated[key] = v;
|
|
253
261
|
props.onChange(updated);
|
|
254
262
|
};
|
|
255
|
-
const child = toReactNode(props.renderChild(field, childValue, childOnChange));
|
|
263
|
+
const child = toReactNode(props.renderChild(field, childValue, childOnChange, key));
|
|
256
264
|
if (child === null || child === void 0) return null;
|
|
257
265
|
return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsxs("label", {
|
|
258
266
|
htmlFor: childId,
|
|
@@ -318,9 +326,9 @@ function renderRecord(props) {
|
|
|
318
326
|
"aria-label": props.meta.description ?? "Record",
|
|
319
327
|
children: entries.map(([key, value]) => {
|
|
320
328
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
321
|
-
htmlFor: inputId(
|
|
329
|
+
htmlFor: inputId(`${props.path}.${key}`),
|
|
322
330
|
children: key
|
|
323
|
-
}), toReactNode(props.renderChild(valueType, value, () => {}))] }, key);
|
|
331
|
+
}), toReactNode(props.renderChild(valueType, value, () => {}, key))] }, key);
|
|
324
332
|
})
|
|
325
333
|
});
|
|
326
334
|
}
|
|
@@ -355,7 +363,7 @@ function renderRecord(props) {
|
|
|
355
363
|
children: [entries.map(([key, value]) => {
|
|
356
364
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
357
365
|
/* @__PURE__ */ jsx("input", {
|
|
358
|
-
id: `${inputId(
|
|
366
|
+
id: `${inputId(`${props.path}.${key}`)}-key`,
|
|
359
367
|
type: "text",
|
|
360
368
|
"aria-label": "Entry key",
|
|
361
369
|
defaultValue: key,
|
|
@@ -365,7 +373,7 @@ function renderRecord(props) {
|
|
|
365
373
|
}),
|
|
366
374
|
toReactNode(props.renderChild(valueType, value, (nextValue) => {
|
|
367
375
|
handleValueChange(key, nextValue);
|
|
368
|
-
})),
|
|
376
|
+
}, key)),
|
|
369
377
|
/* @__PURE__ */ jsx("button", {
|
|
370
378
|
type: "button",
|
|
371
379
|
"aria-label": `Remove entry ${key}`,
|
|
@@ -397,7 +405,7 @@ function renderArray(props) {
|
|
|
397
405
|
next[i] = v;
|
|
398
406
|
props.onChange(next);
|
|
399
407
|
};
|
|
400
|
-
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
408
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange, `[${String(i)}]`)) }, String(i));
|
|
401
409
|
})
|
|
402
410
|
});
|
|
403
411
|
}
|
|
@@ -467,14 +475,20 @@ function discriminatedUnionValueForTab(optionLabels, discKey, newIndex) {
|
|
|
467
475
|
}
|
|
468
476
|
/**
|
|
469
477
|
* WAI-ARIA tabs component for discriminated unions.
|
|
470
|
-
*
|
|
471
|
-
* -
|
|
472
|
-
*
|
|
473
|
-
* -
|
|
474
|
-
* -
|
|
478
|
+
*
|
|
479
|
+
* Implements the WAI-ARIA "Tabs with Automatic Activation" pattern
|
|
480
|
+
* (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/):
|
|
481
|
+
* - ArrowRight / ArrowLeft move between tabs, wrapping at the extremes
|
|
482
|
+
* - Home / End jump to the first / last tab
|
|
483
|
+
* - aria-selected, aria-controls, role="tablist" / "tab" / "tabpanel"
|
|
484
|
+
* - Roving tabindex: the active tab has tabindex=0, the rest tabindex=-1
|
|
485
|
+
*
|
|
486
|
+
* "Automatic activation" means each arrow key both moves focus and
|
|
487
|
+
* activates the new tab in one step — selection and focus stay aligned.
|
|
475
488
|
*/
|
|
476
489
|
function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
|
|
477
490
|
const tabRefs = useRef([]);
|
|
491
|
+
const pendingFocusRef = useRef(false);
|
|
478
492
|
const handleTabChange = useCallback((newIndex) => {
|
|
479
493
|
const next = discriminatedUnionValueForTab(optionLabels, discKey, newIndex);
|
|
480
494
|
if (next === void 0) return;
|
|
@@ -482,31 +496,31 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
|
|
|
482
496
|
}, [
|
|
483
497
|
optionLabels,
|
|
484
498
|
discKey,
|
|
485
|
-
props
|
|
499
|
+
props.onChange
|
|
486
500
|
]);
|
|
487
|
-
const
|
|
488
|
-
const clamped = (index % options.length + options.length) % options.length;
|
|
489
|
-
tabRefs.current[clamped]?.focus();
|
|
490
|
-
}, [options.length]);
|
|
501
|
+
const wrapIndex = useCallback((index) => (index % options.length + options.length) % options.length, [options.length]);
|
|
491
502
|
const handleKeyDown = useCallback((e) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
e.preventDefault();
|
|
503
|
-
focusTab(options.length - 1);
|
|
504
|
-
}
|
|
503
|
+
let target;
|
|
504
|
+
if (e.key === "ArrowRight") target = wrapIndex(activeIndex + 1);
|
|
505
|
+
else if (e.key === "ArrowLeft") target = wrapIndex(activeIndex - 1);
|
|
506
|
+
else if (e.key === "Home") target = 0;
|
|
507
|
+
else if (e.key === "End") target = options.length - 1;
|
|
508
|
+
if (target === void 0) return;
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
if (target === activeIndex) return;
|
|
511
|
+
pendingFocusRef.current = true;
|
|
512
|
+
handleTabChange(target);
|
|
505
513
|
}, [
|
|
506
514
|
activeIndex,
|
|
507
|
-
|
|
508
|
-
options.length
|
|
515
|
+
handleTabChange,
|
|
516
|
+
options.length,
|
|
517
|
+
wrapIndex
|
|
509
518
|
]);
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (!pendingFocusRef.current) return;
|
|
521
|
+
pendingFocusRef.current = false;
|
|
522
|
+
tabRefs.current[activeIndex]?.focus();
|
|
523
|
+
}, [activeIndex]);
|
|
510
524
|
const activeOption = options[activeIndex];
|
|
511
525
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
512
526
|
role: "tablist",
|
|
@@ -523,6 +537,7 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
|
|
|
523
537
|
},
|
|
524
538
|
type: "button",
|
|
525
539
|
role: "tab",
|
|
540
|
+
id: `${panelId}-tab-${String(i)}`,
|
|
526
541
|
"aria-selected": i === activeIndex ? "true" : void 0,
|
|
527
542
|
"aria-controls": `${panelId}-panel`,
|
|
528
543
|
tabIndex: i === activeIndex ? 0 : -1,
|
|
@@ -608,4 +623,4 @@ function matchUnionOption(options, value) {
|
|
|
608
623
|
if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
|
|
609
624
|
}
|
|
610
625
|
//#endregion
|
|
611
|
-
export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
626
|
+
export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
@@ -69,8 +69,17 @@ interface RenderProps extends BaseFieldProps {
|
|
|
69
69
|
* Render a child field. Theme adapters call this to recursively render
|
|
70
70
|
* nested structures (object fields, array elements, union options).
|
|
71
71
|
* The resolver and rendering context are already wired in.
|
|
72
|
+
*
|
|
73
|
+
* @param tree - The walked field tree for the child
|
|
74
|
+
* @param value - The child's current value
|
|
75
|
+
* @param onChange - Callback receiving the child's next value
|
|
76
|
+
* @param pathSuffix - Path segment from the parent (e.g. "city",
|
|
77
|
+
* "[0]"). Joined to the parent's path with a dot, or substituted
|
|
78
|
+
* when the parent acts as a transparent wrapper (union options).
|
|
79
|
+
* Required for every container — without it children inherit no
|
|
80
|
+
* path and `inputId()` will throw.
|
|
72
81
|
*/
|
|
73
|
-
renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => unknown;
|
|
82
|
+
renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void, pathSuffix?: string) => unknown;
|
|
74
83
|
}
|
|
75
84
|
/**
|
|
76
85
|
* Props for HTML render functions. Extends BaseFieldProps with:
|
|
@@ -90,6 +99,23 @@ interface HtmlRenderProps extends BaseFieldProps {
|
|
|
90
99
|
*/
|
|
91
100
|
renderChild: (tree: WalkedField, value: unknown, pathSuffix?: string) => string;
|
|
92
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the `RenderProps` object handed to a resolver render function or a
|
|
104
|
+
* widget. Used by both the server-side `<SchemaView>` (which has no
|
|
105
|
+
* `onChange`) and the client-side `<SchemaComponent>` (which threads an
|
|
106
|
+
* `onChange` callback).
|
|
107
|
+
*
|
|
108
|
+
* When `onChange` is `undefined` the caller is rendering in read-only mode:
|
|
109
|
+
* a noop `onChange` is wired up, `readOnly` is forced to `true`, and
|
|
110
|
+
* `writeOnly` is forced to `false`. Otherwise the editability is taken
|
|
111
|
+
* from `tree.editability`.
|
|
112
|
+
*
|
|
113
|
+
* The duplicate sibling fields (`enumValues`, `element`, `fields`, etc.)
|
|
114
|
+
* are populated for backwards compatibility with renderers that have not
|
|
115
|
+
* yet migrated to reading from `tree` directly. New renderers should
|
|
116
|
+
* narrow on `tree.type` and read from `tree`.
|
|
117
|
+
*/
|
|
118
|
+
declare function buildRenderProps(tree: WalkedField, value: unknown, onChange: ((next: unknown) => void) | undefined, renderChild: RenderProps["renderChild"], path: string): RenderProps;
|
|
93
119
|
type RenderFunction = (props: RenderProps) => unknown;
|
|
94
120
|
interface ComponentResolver {
|
|
95
121
|
string?: RenderFunction;
|
|
@@ -157,4 +183,4 @@ declare function mergeResolvers(user: ComponentResolver, fallback: ComponentReso
|
|
|
157
183
|
*/
|
|
158
184
|
declare function mergeHtmlResolvers(user: HtmlResolver, fallback: HtmlResolver): HtmlResolver;
|
|
159
185
|
//#endregion
|
|
160
|
-
export { HtmlRenderProps as a, RenderFunction as c,
|
|
186
|
+
export { HtmlRenderProps as a, RenderFunction as c, getHtmlRenderFn as d, getRenderFunction as f, typeToKey as h, HtmlRenderFunction as i, RenderProps as l, mergeResolvers as m, BaseFieldProps as n, HtmlResolver as o, mergeHtmlResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, buildRenderProps as u };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { r as ComponentResolver } from "../renderer-
|
|
1
|
+
import { r as ComponentResolver } from "../renderer-DXo-rXHJ.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/themes/mantine.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Register real Mantine components for the resolver to use.
|
|
6
6
|
* Call once at app startup before rendering.
|
|
7
|
+
*
|
|
8
|
+
* `Text` is required so read-only scalars render as a styled Mantine
|
|
9
|
+
* `<Text>` element instead of a bare `<span>`, matching the visual
|
|
10
|
+
* weight of the editable variants.
|
|
7
11
|
*/
|
|
8
12
|
declare function registerMantineComponents(components: {
|
|
9
13
|
TextInput: React.ElementType;
|
|
@@ -11,6 +15,7 @@ declare function registerMantineComponents(components: {
|
|
|
11
15
|
Switch: React.ElementType;
|
|
12
16
|
Select: React.ElementType;
|
|
13
17
|
Fieldset: React.ElementType;
|
|
18
|
+
Text: React.ElementType;
|
|
14
19
|
}): void;
|
|
15
20
|
declare const mantineResolver: ComponentResolver;
|
|
16
21
|
//#endregion
|