schema-components 1.16.3 → 1.17.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/renderer.d.mts +1 -1
- package/dist/html/a11y.d.mts +1 -1
- package/dist/html/renderToHtml.d.mts +1 -1
- package/dist/html/renderToHtmlStream.d.mts +1 -1
- package/dist/html/renderers.d.mts +1 -1
- package/dist/html/streamRenderers.d.mts +1 -1
- package/dist/openapi/components.mjs +42 -13
- package/dist/react/SchemaComponent.d.mts +32 -4
- package/dist/react/SchemaComponent.mjs +50 -15
- package/dist/react/SchemaView.d.mts +9 -2
- package/dist/react/SchemaView.mjs +17 -11
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +15 -2
- package/dist/react/headlessRenderers.mjs +50 -36
- package/dist/{renderer-BdSqllx5.d.mts → renderer-B3s8o2B8.d.mts} +10 -1
- package/dist/themes/mantine.d.mts +6 -1
- package/dist/themes/mantine.mjs +40 -8
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/mui.mjs +17 -3
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/radix.mjs +41 -9
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +22 -5
- package/package.json +5 -2
package/dist/core/renderer.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as HtmlRenderProps, c as RenderFunction, d as getRenderFunction, f as mergeHtmlResolvers, i as HtmlRenderFunction, l as RenderProps, m as typeToKey, n as BaseFieldProps, o as HtmlResolver, p as mergeResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as getHtmlRenderFn } from "../renderer-
|
|
1
|
+
import { a as HtmlRenderProps, c as RenderFunction, d as getRenderFunction, f as mergeHtmlResolvers, i as HtmlRenderFunction, l as RenderProps, m as typeToKey, n as BaseFieldProps, o as HtmlResolver, p as mergeResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as getHtmlRenderFn } from "../renderer-B3s8o2B8.mjs";
|
|
2
2
|
export { AllConstraints, BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
|
package/dist/html/a11y.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as WalkedField } from "../types-D_5ST7SS.mjs";
|
|
2
|
-
import { t as AllConstraints } from "../renderer-
|
|
2
|
+
import { t as AllConstraints } from "../renderer-B3s8o2B8.mjs";
|
|
3
3
|
import { HtmlAttributes, HtmlNode } from "./html.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/html/a11y.d.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as WalkedField } from "../types-D_5ST7SS.mjs";
|
|
2
|
-
import { o as HtmlResolver } from "../renderer-
|
|
2
|
+
import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/html/renderers.d.ts
|
|
5
5
|
declare function dateInputType(format: string | undefined): string | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { M as WalkedField } from "../types-D_5ST7SS.mjs";
|
|
2
|
-
import { o as HtmlResolver } from "../renderer-
|
|
2
|
+
import { o as HtmlResolver } from "../renderer-B3s8o2B8.mjs";
|
|
3
3
|
import { HtmlElement } from "./html.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/html/streamRenderers.d.ts
|
|
@@ -7,10 +7,24 @@ import { ApiLinks } from "./ApiLinks.mjs";
|
|
|
7
7
|
import { ApiResponseHeaders } from "./ApiResponseHeaders.mjs";
|
|
8
8
|
import { ApiSecurity } from "./ApiSecurity.mjs";
|
|
9
9
|
import { getLinks, getSecurityRequirements, getSecuritySchemes, listCallbacks } from "./parser.mjs";
|
|
10
|
-
import { renderField } from "../react/SchemaComponent.mjs";
|
|
10
|
+
import { joinPath, renderField, sanitisePrefix } from "../react/SchemaComponent.mjs";
|
|
11
11
|
import { getParsed, resolveOperation, resolveParameters, resolveRequestBody, resolveResponse, toDoc } from "./resolve.mjs";
|
|
12
12
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useId } from "react";
|
|
13
14
|
//#region src/openapi/components.tsx
|
|
15
|
+
/**
|
|
16
|
+
* OpenAPI React components with type-safe generics.
|
|
17
|
+
*
|
|
18
|
+
* Render API operations, parameters, request bodies, and response schemas
|
|
19
|
+
* from OpenAPI 3.x documents. When the document is typed `as const`,
|
|
20
|
+
* the `fields` / `overrides` props get full autocomplete.
|
|
21
|
+
*
|
|
22
|
+
* Type safety is enforced at the outer component's props level via
|
|
23
|
+
* conditional types (InferRequestBodyFields, InferResponseFields,
|
|
24
|
+
* InferParameterOverrides). Internally, schemas are extracted and
|
|
25
|
+
* rendered via the walker + headless resolver directly, bypassing
|
|
26
|
+
* SchemaComponent to avoid deferred-conditional-type compatibility issues.
|
|
27
|
+
*/
|
|
14
28
|
function noop() {}
|
|
15
29
|
function renderSchema(schema, rootDocument, options) {
|
|
16
30
|
let jsonSchema;
|
|
@@ -32,8 +46,11 @@ function renderSchema(schema, rootDocument, options) {
|
|
|
32
46
|
rootDocument
|
|
33
47
|
};
|
|
34
48
|
const tree = walk(jsonSchema, walkOpts);
|
|
35
|
-
const
|
|
36
|
-
|
|
49
|
+
const makeRenderChild = (parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
|
|
50
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
51
|
+
return renderField(childTree, childValue, childOnChange, void 0, makeRenderChild(childPath), childPath, options.widgets);
|
|
52
|
+
};
|
|
53
|
+
return renderField(tree, options.value, options.onChange ?? noop, void 0, makeRenderChild(options.rootPath), options.rootPath, options.widgets);
|
|
37
54
|
}
|
|
38
55
|
function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBodyChange, responseValue, meta, requestBodyFields, widgets }) {
|
|
39
56
|
const rootDoc = toDoc(doc);
|
|
@@ -42,6 +59,7 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
|
|
|
42
59
|
const securityReqs = getSecurityRequirements(parsed, path, method);
|
|
43
60
|
const securitySchemes = getSecuritySchemes(parsed);
|
|
44
61
|
const callbacks = listCallbacks(parsed, path, method);
|
|
62
|
+
const instancePrefix = sanitisePrefix(useId());
|
|
45
63
|
return /* @__PURE__ */ jsxs("section", {
|
|
46
64
|
"data-operation": `${method.toUpperCase()} ${path}`,
|
|
47
65
|
children: [
|
|
@@ -57,7 +75,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
|
|
|
57
75
|
parameters: resolved.parameters,
|
|
58
76
|
rootDoc,
|
|
59
77
|
meta,
|
|
60
|
-
widgets
|
|
78
|
+
widgets,
|
|
79
|
+
idPrefix: joinPath(instancePrefix, "params")
|
|
61
80
|
})]
|
|
62
81
|
}),
|
|
63
82
|
resolved.requestBody?.schema !== void 0 && /* @__PURE__ */ jsxs("section", {
|
|
@@ -77,7 +96,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
|
|
|
77
96
|
onChange: onRequestBodyChange,
|
|
78
97
|
fields: requestBodyFields,
|
|
79
98
|
meta,
|
|
80
|
-
widgets
|
|
99
|
+
widgets,
|
|
100
|
+
rootPath: joinPath(instancePrefix, "requestBody")
|
|
81
101
|
})
|
|
82
102
|
]
|
|
83
103
|
}),
|
|
@@ -90,7 +110,8 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
|
|
|
90
110
|
meta,
|
|
91
111
|
widgets,
|
|
92
112
|
path,
|
|
93
|
-
method
|
|
113
|
+
method,
|
|
114
|
+
idPrefix: joinPath(instancePrefix, `response-${response.statusCode}`)
|
|
94
115
|
}, response.statusCode))]
|
|
95
116
|
})
|
|
96
117
|
]
|
|
@@ -99,6 +120,7 @@ function ApiOperation({ schema: doc, path, method, requestBodyValue, onRequestBo
|
|
|
99
120
|
function ApiParameters({ schema: doc, path, method, meta, overrides, widgets }) {
|
|
100
121
|
const rootDoc = toDoc(doc);
|
|
101
122
|
const params = resolveParameters(rootDoc, path, method);
|
|
123
|
+
const instancePrefix = sanitisePrefix(useId());
|
|
102
124
|
if (params.length === 0) return null;
|
|
103
125
|
return /* @__PURE__ */ jsxs("section", {
|
|
104
126
|
"data-parameters": true,
|
|
@@ -107,13 +129,15 @@ function ApiParameters({ schema: doc, path, method, meta, overrides, widgets })
|
|
|
107
129
|
rootDoc,
|
|
108
130
|
overrides,
|
|
109
131
|
meta,
|
|
110
|
-
widgets
|
|
132
|
+
widgets,
|
|
133
|
+
idPrefix: instancePrefix
|
|
111
134
|
})]
|
|
112
135
|
});
|
|
113
136
|
}
|
|
114
137
|
function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fields, widgets }) {
|
|
115
138
|
const rootDoc = toDoc(doc);
|
|
116
139
|
const requestBody = resolveRequestBody(rootDoc, path, method);
|
|
140
|
+
const instancePrefix = sanitisePrefix(useId());
|
|
117
141
|
if (requestBody?.schema === void 0) return null;
|
|
118
142
|
return /* @__PURE__ */ jsxs("section", {
|
|
119
143
|
"data-request-body": true,
|
|
@@ -132,7 +156,8 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
|
|
|
132
156
|
onChange,
|
|
133
157
|
fields,
|
|
134
158
|
meta,
|
|
135
|
-
widgets
|
|
159
|
+
widgets,
|
|
160
|
+
rootPath: instancePrefix
|
|
136
161
|
})
|
|
137
162
|
]
|
|
138
163
|
});
|
|
@@ -140,6 +165,7 @@ function ApiRequestBody({ schema: doc, path, method, value, onChange, meta, fiel
|
|
|
140
165
|
function ApiResponse({ schema: doc, path, method, status, value, meta, fields, widgets }) {
|
|
141
166
|
const rootDoc = toDoc(doc);
|
|
142
167
|
const response = resolveResponse(rootDoc, path, method, status);
|
|
168
|
+
const instancePrefix = sanitisePrefix(useId());
|
|
143
169
|
if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
|
|
144
170
|
"data-status": status,
|
|
145
171
|
children: [
|
|
@@ -156,7 +182,8 @@ function ApiResponse({ schema: doc, path, method, status, value, meta, fields, w
|
|
|
156
182
|
meta,
|
|
157
183
|
widgets,
|
|
158
184
|
path,
|
|
159
|
-
method
|
|
185
|
+
method,
|
|
186
|
+
idPrefix: instancePrefix
|
|
160
187
|
});
|
|
161
188
|
}
|
|
162
189
|
function OperationHeader({ operation }) {
|
|
@@ -173,7 +200,7 @@ function OperationHeader({ operation }) {
|
|
|
173
200
|
})
|
|
174
201
|
] });
|
|
175
202
|
}
|
|
176
|
-
function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
|
|
203
|
+
function ParameterList({ parameters, rootDoc, overrides, meta, widgets, idPrefix }) {
|
|
177
204
|
return /* @__PURE__ */ jsx(Fragment, { children: parameters.map((param) => /* @__PURE__ */ jsxs("div", {
|
|
178
205
|
"data-parameter": param.name,
|
|
179
206
|
children: [
|
|
@@ -187,12 +214,13 @@ function ParameterList({ parameters, rootDoc, overrides, meta, widgets }) {
|
|
|
187
214
|
}),
|
|
188
215
|
renderSchema(param.schema ?? { type: "string" }, rootDoc, {
|
|
189
216
|
meta: buildParamMeta(param, overrides, meta),
|
|
190
|
-
widgets
|
|
217
|
+
widgets,
|
|
218
|
+
rootPath: joinPath(idPrefix, param.name)
|
|
191
219
|
})
|
|
192
220
|
]
|
|
193
221
|
}, param.name)) });
|
|
194
222
|
}
|
|
195
|
-
function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, method }) {
|
|
223
|
+
function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, method, idPrefix }) {
|
|
196
224
|
if (response.schema === void 0) return /* @__PURE__ */ jsxs("div", {
|
|
197
225
|
"data-status": response.statusCode,
|
|
198
226
|
children: [
|
|
@@ -217,7 +245,8 @@ function ResponseCard({ response, rootDoc, value, fields, meta, widgets, path, m
|
|
|
217
245
|
readOnly: true,
|
|
218
246
|
...meta
|
|
219
247
|
},
|
|
220
|
-
widgets
|
|
248
|
+
widgets,
|
|
249
|
+
rootPath: idPrefix
|
|
221
250
|
}),
|
|
222
251
|
/* @__PURE__ */ jsx(ApiResponseHeaders, { headers: response.headers }),
|
|
223
252
|
/* @__PURE__ */ jsx(ApiLinks, { links })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { M as WalkedField, T as SchemaMeta, d as FieldOverrides, u as FieldOverride } from "../types-D_5ST7SS.mjs";
|
|
2
2
|
import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
|
|
3
3
|
import { t as SchemaError } from "../errors-C5zRC2PU.mjs";
|
|
4
|
-
import { l as RenderProps, r as ComponentResolver } from "../renderer-
|
|
4
|
+
import { l as RenderProps, r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
|
|
5
5
|
import { c as ResolveOpenAPIRef, s as PathOfType, t as FromJSONSchema } from "../typeInference-k7FXfTVO.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
@@ -70,6 +70,13 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
|
|
|
70
70
|
description?: string;
|
|
71
71
|
/** Instance-scoped widgets — override context and global widgets. */
|
|
72
72
|
widgets?: WidgetMap;
|
|
73
|
+
/**
|
|
74
|
+
* Prefix used for every input `id`/label `htmlFor` in this component
|
|
75
|
+
* subtree. Defaults to a per-instance value from `useId()` so multiple
|
|
76
|
+
* `<SchemaComponent>` instances on the same page never collide. Override
|
|
77
|
+
* for deterministic ids in screenshot tests.
|
|
78
|
+
*/
|
|
79
|
+
idPrefix?: string;
|
|
73
80
|
}
|
|
74
81
|
declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined>({
|
|
75
82
|
schema: schemaInput,
|
|
@@ -86,9 +93,30 @@ declare function SchemaComponent<T = unknown, Ref extends string | undefined = u
|
|
|
86
93
|
readOnly,
|
|
87
94
|
writeOnly,
|
|
88
95
|
description,
|
|
89
|
-
widgets: instanceWidgets
|
|
96
|
+
widgets: instanceWidgets,
|
|
97
|
+
idPrefix
|
|
90
98
|
}: SchemaComponentProps<T, Ref>): ReactNode;
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Default root-path sentinel used when no `idPrefix` is supplied AND the
|
|
101
|
+
* component is rendered outside a React tree (e.g. server-side bundling
|
|
102
|
+
* test harnesses). Production callers receive a `useId()`-derived prefix
|
|
103
|
+
* that is unique per instance.
|
|
104
|
+
*/
|
|
105
|
+
declare const ROOT_PATH = "root";
|
|
106
|
+
/**
|
|
107
|
+
* Append a child path suffix to a parent path. When the suffix is omitted
|
|
108
|
+
* (e.g. transparent wrappers like union options), the parent path is
|
|
109
|
+
* returned unchanged so the child inherits the parent's id.
|
|
110
|
+
*/
|
|
111
|
+
declare function joinPath(parent: string, suffix: string | undefined): string;
|
|
112
|
+
/**
|
|
113
|
+
* Normalise a `useId()` value into a DOM-id-safe prefix. React's `useId`
|
|
114
|
+
* returns values containing `:` characters (e.g. `«:r0:»`) which are
|
|
115
|
+
* invalid in CSS selectors. Replace any run of non-alphanumeric characters
|
|
116
|
+
* with a single hyphen and trim leading/trailing hyphens.
|
|
117
|
+
*/
|
|
118
|
+
declare function sanitisePrefix(value: string): string;
|
|
119
|
+
declare function renderField(tree: WalkedField, value: unknown, onChange: (v: unknown) => void, userResolver: ComponentResolver | undefined, renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void, pathSuffix?: string) => ReactNode, path: string, instanceWidgets?: WidgetMap, contextWidgets?: WidgetMap, depth?: number): ReactNode;
|
|
92
120
|
/**
|
|
93
121
|
* Infer the schema's output type for SchemaField path inference.
|
|
94
122
|
*/
|
|
@@ -125,4 +153,4 @@ declare function SchemaField<T = unknown, Ref extends string | undefined = undef
|
|
|
125
153
|
onValidationError
|
|
126
154
|
}: SchemaFieldProps<T, Ref, P>): ReactNode;
|
|
127
155
|
//#endregion
|
|
128
|
-
export { SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, registerWidget, renderField };
|
|
156
|
+
export { ROOT_PATH, SchemaComponent, SchemaComponentProps, SchemaField, SchemaFieldProps, SchemaProvider, WidgetMap, joinPath, registerWidget, renderField, sanitisePrefix };
|
|
@@ -8,7 +8,7 @@ import { headlessResolver } from "./headless.mjs";
|
|
|
8
8
|
import { resolvePath, resolveValue, setNestedValue } from "./fieldPath.mjs";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { createContext, isValidElement, useCallback, useContext, useMemo } from "react";
|
|
11
|
+
import { createContext, isValidElement, useCallback, useContext, useId, 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,7 +203,7 @@ 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) {
|
|
206
|
+
function buildRenderProps(tree, value, onChange, renderChild, path) {
|
|
175
207
|
const props = {
|
|
176
208
|
value,
|
|
177
209
|
onChange,
|
|
@@ -179,7 +211,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
|
|
|
179
211
|
writeOnly: tree.editability === "input",
|
|
180
212
|
meta: tree.meta,
|
|
181
213
|
constraints: tree.constraints,
|
|
182
|
-
path
|
|
214
|
+
path,
|
|
183
215
|
tree,
|
|
184
216
|
renderChild
|
|
185
217
|
};
|
|
@@ -203,6 +235,7 @@ function buildRenderProps(tree, value, onChange, renderChild) {
|
|
|
203
235
|
function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange, meta: fieldMeta, validate, onValidationError }) {
|
|
204
236
|
const userResolver = useContext(UserResolverContext);
|
|
205
237
|
const contextWidgets = useContext(WidgetsContext);
|
|
238
|
+
const generatedId = useId();
|
|
206
239
|
let jsonSchema;
|
|
207
240
|
let zodSchema;
|
|
208
241
|
let rootMeta;
|
|
@@ -240,10 +273,12 @@ function SchemaField({ path, schema: schemaInput, ref: refInput, value, onChange
|
|
|
240
273
|
onChange,
|
|
241
274
|
onValidationError
|
|
242
275
|
]);
|
|
243
|
-
const makeRenderChild = (currentDepth) => (childTree, childValue, childOnChange) => {
|
|
244
|
-
|
|
276
|
+
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, childOnChange, pathSuffix) => {
|
|
277
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
278
|
+
return renderField(childTree, childValue, childOnChange, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, void 0, contextWidgets, currentDepth + 1);
|
|
245
279
|
};
|
|
246
|
-
|
|
280
|
+
const rootPath = joinPath(sanitisePrefix(generatedId), path);
|
|
281
|
+
return renderField(fieldTree, fieldValue, handleChange, userResolver, makeRenderChild(0, rootPath), rootPath, void 0, contextWidgets, 0);
|
|
247
282
|
}
|
|
248
283
|
/**
|
|
249
284
|
* Dispatch Zod errors to per-field onValidationError callbacks.
|
|
@@ -290,4 +325,4 @@ function detectNormalisationKind(err) {
|
|
|
290
325
|
return "unknown";
|
|
291
326
|
}
|
|
292
327
|
//#endregion
|
|
293
|
-
export { SchemaComponent, SchemaField, SchemaProvider, registerWidget, renderField };
|
|
328
|
+
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
2
|
import { t as Diagnostic } from "../diagnostics-DzbZmcLI.mjs";
|
|
3
|
-
import { r as ComponentResolver } from "../renderer-
|
|
3
|
+
import { r as ComponentResolver } from "../renderer-B3s8o2B8.mjs";
|
|
4
4
|
import { WidgetMap } from "./SchemaComponent.mjs";
|
|
5
5
|
import { ReactNode } from "react";
|
|
6
6
|
|
|
@@ -30,6 +30,12 @@ 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
41
|
* Server-safe schema renderer — no hooks, no context, no state.
|
|
@@ -47,7 +53,8 @@ declare function SchemaView({
|
|
|
47
53
|
resolver,
|
|
48
54
|
widgets,
|
|
49
55
|
onDiagnostic,
|
|
50
|
-
strict
|
|
56
|
+
strict,
|
|
57
|
+
idPrefix
|
|
51
58
|
}: SchemaViewProps): ReactNode;
|
|
52
59
|
//#endregion
|
|
53
60
|
export { SchemaView, SchemaViewProps };
|
|
@@ -3,8 +3,9 @@ import { normaliseSchema } from "../core/adapter.mjs";
|
|
|
3
3
|
import { 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";
|
|
6
7
|
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import { createElement, isValidElement } from "react";
|
|
8
|
+
import { createElement, isValidElement, useId } from "react";
|
|
8
9
|
//#region src/react/SchemaView.tsx
|
|
9
10
|
/**
|
|
10
11
|
* React Server Component for read-only schema rendering.
|
|
@@ -37,7 +38,9 @@ function noop() {}
|
|
|
37
38
|
* Always renders in read-only mode. For editable forms, use
|
|
38
39
|
* `<SchemaComponent>` with `"use client"`.
|
|
39
40
|
*/
|
|
40
|
-
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict }) {
|
|
41
|
+
function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver, widgets, onDiagnostic, strict, idPrefix }) {
|
|
42
|
+
const generatedId = useId();
|
|
43
|
+
const rootPath = idPrefix ?? sanitisePrefix(generatedId);
|
|
41
44
|
const mergedMeta = {
|
|
42
45
|
...componentMeta,
|
|
43
46
|
readOnly: true
|
|
@@ -68,18 +71,21 @@ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: c
|
|
|
68
71
|
const tree = walk(jsonSchema, walkOptions);
|
|
69
72
|
const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
|
|
70
73
|
const MAX_SERVER_DEPTH = 10;
|
|
71
|
-
const makeRenderChild = (currentDepth) => (childTree, childValue) => {
|
|
74
|
+
const makeRenderChild = (currentDepth, parentPath) => (childTree, childValue, pathSuffix) => {
|
|
75
|
+
const childPath = joinPath(parentPath, pathSuffix);
|
|
72
76
|
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);
|
|
77
|
+
return renderFieldServer(childTree, childValue, userResolver, makeRenderChild(currentDepth + 1, childPath), childPath, widgets);
|
|
74
78
|
};
|
|
75
|
-
const renderChild = makeRenderChild(0);
|
|
76
|
-
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, widgets);
|
|
79
|
+
const renderChild = makeRenderChild(0, rootPath);
|
|
80
|
+
return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild, rootPath, widgets);
|
|
77
81
|
}
|
|
78
|
-
function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
82
|
+
function renderFieldServer(tree, value, resolver, renderChild, path, widgets) {
|
|
83
|
+
if (path.length === 0) throw new Error("renderFieldServer requires a non-empty path. Pass ROOT_PATH at the root and join children via joinPath().");
|
|
79
84
|
const componentHint = tree.meta.component;
|
|
80
85
|
if (typeof componentHint === "string") {
|
|
81
86
|
const widget = widgets?.get(componentHint);
|
|
82
87
|
if (widget !== void 0) {
|
|
88
|
+
const wrapRenderChild = (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix);
|
|
83
89
|
const result = widget({
|
|
84
90
|
value,
|
|
85
91
|
onChange: noop,
|
|
@@ -87,9 +93,9 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
|
87
93
|
writeOnly: false,
|
|
88
94
|
meta: tree.meta,
|
|
89
95
|
constraints: tree.constraints,
|
|
90
|
-
path
|
|
96
|
+
path,
|
|
91
97
|
tree,
|
|
92
|
-
renderChild:
|
|
98
|
+
renderChild: wrapRenderChild
|
|
93
99
|
});
|
|
94
100
|
if (result !== void 0 && result !== null) {
|
|
95
101
|
if (isValidElement(result)) return result;
|
|
@@ -106,9 +112,9 @@ function renderFieldServer(tree, value, resolver, renderChild, widgets) {
|
|
|
106
112
|
writeOnly: false,
|
|
107
113
|
meta: tree.meta,
|
|
108
114
|
constraints: tree.constraints,
|
|
109
|
-
path
|
|
115
|
+
path,
|
|
110
116
|
tree,
|
|
111
|
-
renderChild: (childTree, childValue) => renderChild(childTree, childValue)
|
|
117
|
+
renderChild: (childTree, childValue, _childOnChange, pathSuffix) => renderChild(childTree, childValue, pathSuffix)
|
|
112
118
|
};
|
|
113
119
|
if (tree.type === "enum") props.enumValues = tree.enumValues;
|
|
114
120
|
if (tree.type === "array" && tree.element !== void 0) props.element = tree.element;
|
|
@@ -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-B3s8o2B8.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
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { isValidElement, useCallback, useRef } from "react";
|
|
3
|
+
import { isValidElement, useCallback, useEffect, 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;
|
|
@@ -484,29 +498,29 @@ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, d
|
|
|
484
498
|
discKey,
|
|
485
499
|
props
|
|
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",
|
|
@@ -608,4 +622,4 @@ function matchUnionOption(options, value) {
|
|
|
608
622
|
if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
|
|
609
623
|
}
|
|
610
624
|
//#endregion
|
|
611
|
-
export { defaultRecordValue, discriminatedUnionValueForTab, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
625
|
+
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:
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { r as ComponentResolver } from "../renderer-
|
|
1
|
+
import { r as ComponentResolver } from "../renderer-B3s8o2B8.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
|
package/dist/themes/mantine.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
-
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
2
|
+
import { inputId, toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
3
|
import { headlessResolver } from "../react/headless.mjs";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/themes/mantine.tsx
|
|
@@ -17,9 +17,14 @@ let MantineSwitch = (props) => /* @__PURE__ */ jsx("input", {
|
|
|
17
17
|
});
|
|
18
18
|
let MantineSelect = (props) => /* @__PURE__ */ jsx("select", { ...props });
|
|
19
19
|
let MantineFieldset = (props) => /* @__PURE__ */ jsx("fieldset", { ...props });
|
|
20
|
+
let MantineText = (props) => /* @__PURE__ */ jsx("span", { ...props });
|
|
20
21
|
/**
|
|
21
22
|
* Register real Mantine components for the resolver to use.
|
|
22
23
|
* Call once at app startup before rendering.
|
|
24
|
+
*
|
|
25
|
+
* `Text` is required so read-only scalars render as a styled Mantine
|
|
26
|
+
* `<Text>` element instead of a bare `<span>`, matching the visual
|
|
27
|
+
* weight of the editable variants.
|
|
23
28
|
*/
|
|
24
29
|
function registerMantineComponents(components) {
|
|
25
30
|
MantineTextInput = components.TextInput;
|
|
@@ -27,12 +32,18 @@ function registerMantineComponents(components) {
|
|
|
27
32
|
MantineSwitch = components.Switch;
|
|
28
33
|
MantineSelect = components.Select;
|
|
29
34
|
MantineFieldset = components.Fieldset;
|
|
35
|
+
MantineText = components.Text;
|
|
30
36
|
}
|
|
31
37
|
function renderStringInput(props) {
|
|
32
38
|
const strValue = typeof props.value === "string" ? props.value : "";
|
|
33
39
|
const label = getLabel(props);
|
|
34
|
-
|
|
40
|
+
const id = inputId(props.path);
|
|
41
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(MantineText, {
|
|
42
|
+
id,
|
|
43
|
+
children: strValue || "—"
|
|
44
|
+
});
|
|
35
45
|
return /* @__PURE__ */ jsx(MantineTextInput, {
|
|
46
|
+
id,
|
|
36
47
|
label,
|
|
37
48
|
value: props.writeOnly ? "" : strValue,
|
|
38
49
|
onChange: (e) => {
|
|
@@ -42,11 +53,19 @@ function renderStringInput(props) {
|
|
|
42
53
|
}
|
|
43
54
|
function renderNumberInput(props) {
|
|
44
55
|
const label = getLabel(props);
|
|
56
|
+
const id = inputId(props.path);
|
|
45
57
|
if (props.readOnly) {
|
|
46
|
-
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(
|
|
47
|
-
|
|
58
|
+
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(MantineText, {
|
|
59
|
+
id,
|
|
60
|
+
children: "—"
|
|
61
|
+
});
|
|
62
|
+
return /* @__PURE__ */ jsx(MantineText, {
|
|
63
|
+
id,
|
|
64
|
+
children: props.value.toLocaleString()
|
|
65
|
+
});
|
|
48
66
|
}
|
|
49
67
|
return /* @__PURE__ */ jsx(MantineNumberInput, {
|
|
68
|
+
id,
|
|
50
69
|
label,
|
|
51
70
|
value: props.writeOnly ? void 0 : typeof props.value === "number" ? props.value : void 0,
|
|
52
71
|
onChange: (v) => {
|
|
@@ -56,11 +75,19 @@ function renderNumberInput(props) {
|
|
|
56
75
|
}
|
|
57
76
|
function renderBooleanInput(props) {
|
|
58
77
|
const label = getLabel(props);
|
|
78
|
+
const id = inputId(props.path);
|
|
59
79
|
if (props.readOnly) {
|
|
60
|
-
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(
|
|
61
|
-
|
|
80
|
+
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(MantineText, {
|
|
81
|
+
id,
|
|
82
|
+
children: "—"
|
|
83
|
+
});
|
|
84
|
+
return /* @__PURE__ */ jsx(MantineText, {
|
|
85
|
+
id,
|
|
86
|
+
children: props.value ? "Yes" : "No"
|
|
87
|
+
});
|
|
62
88
|
}
|
|
63
89
|
return /* @__PURE__ */ jsx(MantineSwitch, {
|
|
90
|
+
id,
|
|
64
91
|
label,
|
|
65
92
|
checked: props.writeOnly ? false : props.value === true,
|
|
66
93
|
onChange: (e) => {
|
|
@@ -71,8 +98,13 @@ function renderBooleanInput(props) {
|
|
|
71
98
|
function renderEnumInput(props) {
|
|
72
99
|
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
73
100
|
const label = getLabel(props);
|
|
74
|
-
|
|
101
|
+
const id = inputId(props.path);
|
|
102
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(MantineText, {
|
|
103
|
+
id,
|
|
104
|
+
children: enumValue || "—"
|
|
105
|
+
});
|
|
75
106
|
return /* @__PURE__ */ jsx(MantineSelect, {
|
|
107
|
+
id,
|
|
76
108
|
label,
|
|
77
109
|
value: props.writeOnly ? null : enumValue || null,
|
|
78
110
|
onChange: (v) => {
|
|
@@ -100,7 +132,7 @@ function renderObjectContainer(props) {
|
|
|
100
132
|
};
|
|
101
133
|
return /* @__PURE__ */ jsx("div", {
|
|
102
134
|
style: { marginBottom: "0.5rem" },
|
|
103
|
-
children: toReactNode(props.renderChild(field, childValue, childOnChange))
|
|
135
|
+
children: toReactNode(props.renderChild(field, childValue, childOnChange, key))
|
|
104
136
|
}, key);
|
|
105
137
|
})
|
|
106
138
|
});
|
package/dist/themes/mui.d.mts
CHANGED
package/dist/themes/mui.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
-
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
2
|
+
import { inputId, toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
3
|
import { headlessResolver } from "../react/headless.mjs";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { isValidElement } from "react";
|
|
@@ -10,11 +10,14 @@ function ariaRequired(tree) {
|
|
|
10
10
|
function renderStringInput(props) {
|
|
11
11
|
const strValue = typeof props.value === "string" ? props.value : "";
|
|
12
12
|
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
13
|
+
const id = inputId(props.path);
|
|
13
14
|
if (props.readOnly) return /* @__PURE__ */ jsx(MuiTypography, {
|
|
15
|
+
id,
|
|
14
16
|
variant: "body2",
|
|
15
17
|
children: strValue || "—"
|
|
16
18
|
});
|
|
17
19
|
return /* @__PURE__ */ jsx(MuiTextField, {
|
|
20
|
+
id,
|
|
18
21
|
label,
|
|
19
22
|
type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
|
|
20
23
|
value: props.writeOnly ? "" : strValue,
|
|
@@ -33,17 +36,21 @@ function renderStringInput(props) {
|
|
|
33
36
|
}
|
|
34
37
|
function renderNumberInput(props) {
|
|
35
38
|
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
39
|
+
const id = inputId(props.path);
|
|
36
40
|
if (props.readOnly) {
|
|
37
41
|
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(MuiTypography, {
|
|
42
|
+
id,
|
|
38
43
|
variant: "body2",
|
|
39
44
|
children: "—"
|
|
40
45
|
});
|
|
41
46
|
return /* @__PURE__ */ jsx(MuiTypography, {
|
|
47
|
+
id,
|
|
42
48
|
variant: "body2",
|
|
43
49
|
children: props.value.toLocaleString()
|
|
44
50
|
});
|
|
45
51
|
}
|
|
46
52
|
return /* @__PURE__ */ jsx(MuiTextField, {
|
|
53
|
+
id,
|
|
47
54
|
label,
|
|
48
55
|
type: "number",
|
|
49
56
|
value: props.writeOnly ? "" : typeof props.value === "number" ? props.value : "",
|
|
@@ -62,18 +69,22 @@ function renderNumberInput(props) {
|
|
|
62
69
|
}
|
|
63
70
|
function renderBooleanInput(props) {
|
|
64
71
|
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
72
|
+
const id = inputId(props.path);
|
|
65
73
|
if (props.readOnly) {
|
|
66
74
|
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(MuiTypography, {
|
|
75
|
+
id,
|
|
67
76
|
variant: "body2",
|
|
68
77
|
children: "—"
|
|
69
78
|
});
|
|
70
79
|
return /* @__PURE__ */ jsx(MuiTypography, {
|
|
80
|
+
id,
|
|
71
81
|
variant: "body2",
|
|
72
82
|
children: props.value ? "Yes" : "No"
|
|
73
83
|
});
|
|
74
84
|
}
|
|
75
85
|
return /* @__PURE__ */ jsx(MuiFormControlLabel, {
|
|
76
86
|
control: /* @__PURE__ */ jsx(MuiCheckbox, {
|
|
87
|
+
id,
|
|
77
88
|
checked: props.writeOnly ? false : props.value === true,
|
|
78
89
|
onChange: (e) => {
|
|
79
90
|
props.onChange(e.target.checked);
|
|
@@ -85,11 +96,14 @@ function renderBooleanInput(props) {
|
|
|
85
96
|
function renderEnumInput(props) {
|
|
86
97
|
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
87
98
|
const label = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
99
|
+
const id = inputId(props.path);
|
|
88
100
|
if (props.readOnly) return /* @__PURE__ */ jsx(MuiTypography, {
|
|
101
|
+
id,
|
|
89
102
|
variant: "body2",
|
|
90
103
|
children: enumValue || "—"
|
|
91
104
|
});
|
|
92
105
|
return /* @__PURE__ */ jsxs(MuiTextField, {
|
|
106
|
+
id,
|
|
93
107
|
select: true,
|
|
94
108
|
label,
|
|
95
109
|
value: props.writeOnly ? "" : enumValue,
|
|
@@ -130,7 +144,7 @@ function renderObjectContainer(props) {
|
|
|
130
144
|
updated[key] = v;
|
|
131
145
|
props.onChange(updated);
|
|
132
146
|
};
|
|
133
|
-
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(field, childValue, childOnChange)) }, key);
|
|
147
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(field, childValue, childOnChange, key)) }, key);
|
|
134
148
|
})]
|
|
135
149
|
});
|
|
136
150
|
}
|
|
@@ -150,7 +164,7 @@ function renderArrayContainer(props) {
|
|
|
150
164
|
next[i] = v;
|
|
151
165
|
props.onChange(next);
|
|
152
166
|
};
|
|
153
|
-
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
167
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange, `[${String(i)}]`)) }, String(i));
|
|
154
168
|
})
|
|
155
169
|
});
|
|
156
170
|
}
|
package/dist/themes/radix.d.mts
CHANGED
package/dist/themes/radix.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
-
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
2
|
+
import { inputId, toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
3
|
import { headlessResolver } from "../react/headless.mjs";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/themes/radix.tsx
|
|
@@ -41,13 +41,19 @@ function registerRadixComponents(components) {
|
|
|
41
41
|
function renderStringInput(props) {
|
|
42
42
|
const strValue = typeof props.value === "string" ? props.value : "";
|
|
43
43
|
const label = getLabel(props);
|
|
44
|
-
|
|
44
|
+
const id = inputId(props.path);
|
|
45
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(RadixText, {
|
|
46
|
+
id,
|
|
47
|
+
children: strValue || "—"
|
|
48
|
+
});
|
|
45
49
|
return /* @__PURE__ */ jsxs(RadixBox, { children: [label !== void 0 && /* @__PURE__ */ jsx(RadixText, {
|
|
46
50
|
as: "label",
|
|
47
51
|
size: "2",
|
|
48
52
|
weight: "medium",
|
|
53
|
+
htmlFor: id,
|
|
49
54
|
children: label
|
|
50
55
|
}), /* @__PURE__ */ jsx(RadixTextField, {
|
|
56
|
+
id,
|
|
51
57
|
type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
|
|
52
58
|
value: props.writeOnly ? "" : strValue,
|
|
53
59
|
onChange: (e) => {
|
|
@@ -58,16 +64,25 @@ function renderStringInput(props) {
|
|
|
58
64
|
}
|
|
59
65
|
function renderNumberInput(props) {
|
|
60
66
|
const label = getLabel(props);
|
|
67
|
+
const id = inputId(props.path);
|
|
61
68
|
if (props.readOnly) {
|
|
62
|
-
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(RadixText, {
|
|
63
|
-
|
|
69
|
+
if (typeof props.value !== "number") return /* @__PURE__ */ jsx(RadixText, {
|
|
70
|
+
id,
|
|
71
|
+
children: "—"
|
|
72
|
+
});
|
|
73
|
+
return /* @__PURE__ */ jsx(RadixText, {
|
|
74
|
+
id,
|
|
75
|
+
children: props.value.toLocaleString()
|
|
76
|
+
});
|
|
64
77
|
}
|
|
65
78
|
return /* @__PURE__ */ jsxs(RadixBox, { children: [label !== void 0 && /* @__PURE__ */ jsx(RadixText, {
|
|
66
79
|
as: "label",
|
|
67
80
|
size: "2",
|
|
68
81
|
weight: "medium",
|
|
82
|
+
htmlFor: id,
|
|
69
83
|
children: label
|
|
70
84
|
}), /* @__PURE__ */ jsx(RadixTextField, {
|
|
85
|
+
id,
|
|
71
86
|
type: "number",
|
|
72
87
|
value: props.writeOnly ? "" : typeof props.value === "number" ? props.value : "",
|
|
73
88
|
onChange: (e) => {
|
|
@@ -78,20 +93,29 @@ function renderNumberInput(props) {
|
|
|
78
93
|
}
|
|
79
94
|
function renderBooleanInput(props) {
|
|
80
95
|
const label = getLabel(props);
|
|
96
|
+
const id = inputId(props.path);
|
|
81
97
|
if (props.readOnly) {
|
|
82
|
-
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(RadixText, {
|
|
83
|
-
|
|
98
|
+
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx(RadixText, {
|
|
99
|
+
id,
|
|
100
|
+
children: "—"
|
|
101
|
+
});
|
|
102
|
+
return /* @__PURE__ */ jsx(RadixText, {
|
|
103
|
+
id,
|
|
104
|
+
children: props.value ? "Yes" : "No"
|
|
105
|
+
});
|
|
84
106
|
}
|
|
85
107
|
return /* @__PURE__ */ jsxs(RadixFlex, {
|
|
86
108
|
align: "center",
|
|
87
109
|
gap: "2",
|
|
88
110
|
children: [/* @__PURE__ */ jsx(RadixCheckbox, {
|
|
111
|
+
id,
|
|
89
112
|
checked: props.writeOnly ? false : props.value === true,
|
|
90
113
|
onCheckedChange: (checked) => {
|
|
91
114
|
if (typeof checked === "boolean") props.onChange(checked);
|
|
92
115
|
}
|
|
93
116
|
}), label !== void 0 && /* @__PURE__ */ jsx(RadixText, {
|
|
94
117
|
as: "label",
|
|
118
|
+
htmlFor: id,
|
|
95
119
|
children: label
|
|
96
120
|
})]
|
|
97
121
|
});
|
|
@@ -99,18 +123,26 @@ function renderBooleanInput(props) {
|
|
|
99
123
|
function renderEnumInput(props) {
|
|
100
124
|
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
101
125
|
const label = getLabel(props);
|
|
102
|
-
|
|
126
|
+
const id = inputId(props.path);
|
|
127
|
+
if (props.readOnly) return /* @__PURE__ */ jsx(RadixText, {
|
|
128
|
+
id,
|
|
129
|
+
children: enumValue || "—"
|
|
130
|
+
});
|
|
103
131
|
return /* @__PURE__ */ jsxs(RadixBox, { children: [label !== void 0 && /* @__PURE__ */ jsx(RadixText, {
|
|
104
132
|
as: "label",
|
|
105
133
|
size: "2",
|
|
106
134
|
weight: "medium",
|
|
135
|
+
htmlFor: id,
|
|
107
136
|
children: label
|
|
108
137
|
}), /* @__PURE__ */ jsxs(RadixSelectRoot, {
|
|
109
138
|
value: props.writeOnly ? "" : enumValue,
|
|
110
139
|
onValueChange: (value) => {
|
|
111
140
|
props.onChange(value);
|
|
112
141
|
},
|
|
113
|
-
children: [/* @__PURE__ */ jsx(RadixSelectTrigger, {
|
|
142
|
+
children: [/* @__PURE__ */ jsx(RadixSelectTrigger, {
|
|
143
|
+
id,
|
|
144
|
+
mt: "1"
|
|
145
|
+
}), /* @__PURE__ */ jsx(RadixSelectContent, { children: (props.enumValues ?? []).map((value) => /* @__PURE__ */ jsx(RadixSelectItem, {
|
|
114
146
|
value,
|
|
115
147
|
children: value
|
|
116
148
|
}, value)) })]
|
|
@@ -137,7 +169,7 @@ function renderObjectContainer(props) {
|
|
|
137
169
|
updated[key] = v;
|
|
138
170
|
props.onChange(updated);
|
|
139
171
|
};
|
|
140
|
-
return /* @__PURE__ */ jsx(RadixBox, { children: toReactNode(props.renderChild(field, childValue, childOnChange)) }, key);
|
|
172
|
+
return /* @__PURE__ */ jsx(RadixBox, { children: toReactNode(props.renderChild(field, childValue, childOnChange, key)) }, key);
|
|
141
173
|
})
|
|
142
174
|
})] });
|
|
143
175
|
}
|
package/dist/themes/shadcn.d.mts
CHANGED
package/dist/themes/shadcn.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toRecord } from "../core/guards.mjs";
|
|
2
|
-
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
2
|
+
import { inputId, toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
3
|
import { headlessResolver } from "../react/headless.mjs";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/themes/shadcn.tsx
|
|
@@ -11,12 +11,15 @@ function buildClassNames(...classes) {
|
|
|
11
11
|
}
|
|
12
12
|
function renderStringInput(props) {
|
|
13
13
|
const strValue = typeof props.value === "string" ? props.value : "";
|
|
14
|
+
const id = inputId(props.path);
|
|
14
15
|
const className = buildClassNames("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors", "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground", "placeholder:text-muted-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50");
|
|
15
16
|
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
17
|
+
id,
|
|
16
18
|
className: "text-sm",
|
|
17
19
|
children: strValue || "—"
|
|
18
20
|
});
|
|
19
21
|
if (props.writeOnly) return /* @__PURE__ */ jsx("input", {
|
|
22
|
+
id,
|
|
20
23
|
type: props.constraints.format === "email" ? "email" : "text",
|
|
21
24
|
className,
|
|
22
25
|
placeholder: typeof props.meta.description === "string" ? props.meta.description : void 0,
|
|
@@ -26,6 +29,7 @@ function renderStringInput(props) {
|
|
|
26
29
|
}
|
|
27
30
|
});
|
|
28
31
|
return /* @__PURE__ */ jsx("input", {
|
|
32
|
+
id,
|
|
29
33
|
type: props.constraints.format === "email" ? "email" : "text",
|
|
30
34
|
className,
|
|
31
35
|
value: strValue,
|
|
@@ -38,18 +42,22 @@ function renderStringInput(props) {
|
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
function renderNumberInput(props) {
|
|
45
|
+
const id = inputId(props.path);
|
|
41
46
|
const className = buildClassNames("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors", "placeholder:text-muted-foreground", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50");
|
|
42
47
|
if (props.readOnly) {
|
|
43
48
|
if (typeof props.value !== "number") return /* @__PURE__ */ jsx("span", {
|
|
49
|
+
id,
|
|
44
50
|
className: "text-sm",
|
|
45
51
|
children: "—"
|
|
46
52
|
});
|
|
47
53
|
return /* @__PURE__ */ jsx("span", {
|
|
54
|
+
id,
|
|
48
55
|
className: "text-sm",
|
|
49
56
|
children: props.value.toLocaleString()
|
|
50
57
|
});
|
|
51
58
|
}
|
|
52
59
|
return /* @__PURE__ */ jsx("input", {
|
|
60
|
+
id,
|
|
53
61
|
type: "number",
|
|
54
62
|
className,
|
|
55
63
|
value: props.writeOnly ? "" : typeof props.value === "number" ? props.value : "",
|
|
@@ -61,18 +69,22 @@ function renderNumberInput(props) {
|
|
|
61
69
|
});
|
|
62
70
|
}
|
|
63
71
|
function renderBooleanInput(props) {
|
|
72
|
+
const id = inputId(props.path);
|
|
64
73
|
const className = buildClassNames("h-4 w-4 rounded border border-primary shadow", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50");
|
|
65
74
|
if (props.readOnly) {
|
|
66
75
|
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx("span", {
|
|
76
|
+
id,
|
|
67
77
|
className: "text-sm",
|
|
68
78
|
children: "—"
|
|
69
79
|
});
|
|
70
80
|
return /* @__PURE__ */ jsx("span", {
|
|
81
|
+
id,
|
|
71
82
|
className: "text-sm",
|
|
72
83
|
children: props.value ? "Yes" : "No"
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
86
|
return /* @__PURE__ */ jsx("input", {
|
|
87
|
+
id,
|
|
76
88
|
type: "checkbox",
|
|
77
89
|
className,
|
|
78
90
|
checked: props.writeOnly ? false : props.value === true,
|
|
@@ -92,6 +104,7 @@ function renderObjectContainer(props) {
|
|
|
92
104
|
children: props.meta.description
|
|
93
105
|
}), Object.entries(fields).map(([key, field]) => {
|
|
94
106
|
const childValue = toRecord(obj)[key];
|
|
107
|
+
const childId = inputId(`${props.path}.${key}`);
|
|
95
108
|
const childOnChange = (v) => {
|
|
96
109
|
const updated = {};
|
|
97
110
|
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
@@ -101,9 +114,10 @@ function renderObjectContainer(props) {
|
|
|
101
114
|
return /* @__PURE__ */ jsxs("div", {
|
|
102
115
|
className: "space-y-1",
|
|
103
116
|
children: [/* @__PURE__ */ jsx("label", {
|
|
117
|
+
htmlFor: childId,
|
|
104
118
|
className: "text-sm font-medium leading-none",
|
|
105
119
|
children: field.meta.description ?? key
|
|
106
|
-
}), toReactNode(props.renderChild(field, childValue, childOnChange))]
|
|
120
|
+
}), toReactNode(props.renderChild(field, childValue, childOnChange, key))]
|
|
107
121
|
}, key);
|
|
108
122
|
})]
|
|
109
123
|
});
|
|
@@ -120,26 +134,29 @@ function renderArrayContainer(props) {
|
|
|
120
134
|
next[i] = v;
|
|
121
135
|
props.onChange(next);
|
|
122
136
|
};
|
|
123
|
-
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
137
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange, `[${String(i)}]`)) }, String(i));
|
|
124
138
|
})
|
|
125
139
|
});
|
|
126
140
|
}
|
|
127
141
|
function renderEnumInput(props) {
|
|
142
|
+
const id = inputId(props.path);
|
|
128
143
|
const className = buildClassNames("flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm", "focus:outline-none focus:ring-1 focus:ring-ring", "disabled:cursor-not-allowed disabled:opacity-50");
|
|
129
144
|
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
130
145
|
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
146
|
+
id,
|
|
131
147
|
className: "text-sm",
|
|
132
148
|
children: enumValue || "—"
|
|
133
149
|
});
|
|
134
150
|
return /* @__PURE__ */ jsxs("select", {
|
|
151
|
+
id,
|
|
135
152
|
className,
|
|
136
153
|
value: props.writeOnly ? "" : enumValue,
|
|
137
154
|
onChange: (e) => {
|
|
138
155
|
props.onChange(e.target.value);
|
|
139
156
|
},
|
|
140
|
-
children: [/* @__PURE__ */
|
|
157
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
141
158
|
value: "",
|
|
142
|
-
children: "Select
|
|
159
|
+
children: ["Select", "…"]
|
|
143
160
|
}), props.enumValues?.map((v) => {
|
|
144
161
|
const display = v === null ? "null" : typeof v === "string" ? v : String(v);
|
|
145
162
|
return /* @__PURE__ */ jsx("option", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "schema-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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",
|
|
@@ -78,13 +78,16 @@
|
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@eslint/js": "10.0.1",
|
|
81
|
-
"@
|
|
81
|
+
"@testing-library/react": "16.3.2",
|
|
82
|
+
"@testing-library/user-event": "14.6.1",
|
|
83
|
+
"@types/node": "25.6.2",
|
|
82
84
|
"@types/react": "19.2.14",
|
|
83
85
|
"@types/react-dom": "19.2.3",
|
|
84
86
|
"@vitest/coverage-v8": "4.1.5",
|
|
85
87
|
"eslint": "10.3.0",
|
|
86
88
|
"eslint-config-prettier": "10.1.8",
|
|
87
89
|
"eslint-plugin-prettier": "5.5.5",
|
|
90
|
+
"happy-dom": "20.9.0",
|
|
88
91
|
"prettier": "3.8.3",
|
|
89
92
|
"react": "19.2.6",
|
|
90
93
|
"react-dom": "19.2.6",
|