schema-components 1.12.11 → 1.14.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/README.md +35 -0
- package/dist/core/adapter.d.mts +8 -3
- package/dist/core/adapter.mjs +58 -14
- package/dist/core/constraints.d.mts +17 -0
- package/dist/core/constraints.mjs +150 -0
- package/dist/core/diagnostics.d.mts +2 -0
- package/dist/core/diagnostics.mjs +33 -0
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/formats.d.mts +33 -0
- package/dist/core/formats.mjs +67 -0
- package/dist/core/merge.d.mts +48 -0
- package/dist/core/merge.mjs +125 -0
- package/dist/core/normalise.d.mts +41 -0
- package/dist/core/normalise.mjs +2 -0
- package/dist/core/openapi30.d.mts +41 -0
- package/dist/core/openapi30.mjs +2 -0
- package/dist/core/ref.d.mts +2 -0
- package/dist/core/ref.mjs +165 -0
- package/dist/core/renderer.d.mts +2 -2
- package/dist/core/renderer.mjs +8 -0
- package/dist/core/swagger2.d.mts +11 -0
- package/dist/core/swagger2.mjs +2 -0
- package/dist/core/typeInference.d.mts +2 -0
- package/dist/core/typeInference.mjs +1 -0
- package/dist/core/types.d.mts +2 -3
- package/dist/core/types.mjs +58 -2
- package/dist/core/version.d.mts +2 -0
- package/dist/core/version.mjs +151 -0
- package/dist/core/walkBuilders.d.mts +66 -0
- package/dist/core/walkBuilders.mjs +152 -0
- package/dist/core/walker.d.mts +3 -10
- package/dist/core/walker.mjs +245 -233
- package/dist/diagnostics-DzbZmcLI.d.mts +64 -0
- package/dist/html/a11y.d.mts +5 -4
- package/dist/html/renderToHtml.d.mts +3 -3
- package/dist/html/renderToHtml.mjs +23 -379
- package/dist/html/renderToHtmlStream.d.mts +29 -47
- package/dist/html/renderToHtmlStream.mjs +33 -305
- package/dist/html/renderers.d.mts +14 -0
- package/dist/html/renderers.mjs +406 -0
- package/dist/html/streamRenderers.d.mts +13 -0
- package/dist/html/streamRenderers.mjs +243 -0
- package/dist/normalise-tL9FckAk.mjs +748 -0
- package/dist/openapi/ApiCallbacks.d.mts +16 -0
- package/dist/openapi/ApiCallbacks.mjs +34 -0
- package/dist/openapi/ApiLinks.d.mts +16 -0
- package/dist/openapi/ApiLinks.mjs +42 -0
- package/dist/openapi/ApiResponseHeaders.d.mts +16 -0
- package/dist/openapi/ApiResponseHeaders.mjs +35 -0
- package/dist/openapi/ApiSecurity.d.mts +19 -0
- package/dist/openapi/ApiSecurity.mjs +33 -0
- package/dist/openapi/bundle.d.mts +47 -0
- package/dist/openapi/bundle.mjs +95 -0
- package/dist/openapi/components.d.mts +7 -1
- package/dist/openapi/components.mjs +30 -6
- package/dist/openapi/parser.d.mts +59 -2
- package/dist/openapi/parser.mjs +189 -8
- package/dist/react/SchemaComponent.d.mts +13 -4
- package/dist/react/SchemaComponent.mjs +51 -91
- package/dist/react/SchemaView.d.mts +10 -2
- package/dist/react/SchemaView.mjs +33 -15
- package/dist/react/fieldPath.d.mts +20 -0
- package/dist/react/fieldPath.mjs +81 -0
- package/dist/react/headless.d.mts +2 -4
- package/dist/react/headless.mjs +3 -492
- package/dist/react/headlessRenderers.d.mts +23 -0
- package/dist/react/headlessRenderers.mjs +507 -0
- package/dist/ref-DvWoULcy.d.mts +44 -0
- package/dist/renderer-BdSqllx5.d.mts +160 -0
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mantine.mjs +2 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/mui.mjs +3 -2
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/radix.mjs +2 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/themes/shadcn.mjs +10 -6
- package/dist/typeInference-k7FXfTVO.d.mts +335 -0
- package/dist/types-D_5ST7SS.d.mts +269 -0
- package/dist/version-B5NV-35j.d.mts +69 -0
- package/package.json +1 -1
- package/dist/types-BJzEgJdX.d.mts +0 -335
- /package/dist/{errors-DIKI2C78.d.mts → errors-C5zRC2PU.d.mts} +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
//#region src/react/fieldPath.ts
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a dot-separated path through a WalkedField tree.
|
|
5
|
+
* Supports array index notation: `field[0]`.
|
|
6
|
+
*/
|
|
7
|
+
function resolvePath(tree, path) {
|
|
8
|
+
if (path.length === 0) return tree;
|
|
9
|
+
const parts = path.split(".");
|
|
10
|
+
let current = tree;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (current === void 0) return void 0;
|
|
13
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
14
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
15
|
+
const arrayField = bracketMatch[1];
|
|
16
|
+
if (current.type === "object") current = current.fields[arrayField];
|
|
17
|
+
if (current?.type === "array") current = current.element;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (current.type === "object") current = current.fields[part];
|
|
21
|
+
else if (current.type === "array") current = current.element;
|
|
22
|
+
else return;
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a dot-separated path through a data value.
|
|
28
|
+
* Supports array index notation: `field[0]`.
|
|
29
|
+
*/
|
|
30
|
+
function resolveValue(root, path) {
|
|
31
|
+
if (path.length === 0) return root;
|
|
32
|
+
const parts = path.split(".");
|
|
33
|
+
let current = root;
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
if (!isObject(current)) return void 0;
|
|
36
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
37
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
38
|
+
const key = bracketMatch[1];
|
|
39
|
+
const index = Number(bracketMatch[2]);
|
|
40
|
+
const arr = current[key];
|
|
41
|
+
if (Array.isArray(arr)) current = arr[index];
|
|
42
|
+
else return;
|
|
43
|
+
} else current = current[part];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set a value at a dot-separated path, producing a new root object.
|
|
49
|
+
* Does not mutate the input — returns a shallow-updated copy at each level.
|
|
50
|
+
*/
|
|
51
|
+
function setNestedValue(root, path, leafValue) {
|
|
52
|
+
if (path.length === 0) return leafValue;
|
|
53
|
+
const parts = path.split(".");
|
|
54
|
+
const result = isObject(root) ? { ...root } : {};
|
|
55
|
+
let current = result;
|
|
56
|
+
for (let i = 0; i < parts.length; i++) {
|
|
57
|
+
const part = parts[i];
|
|
58
|
+
if (part === void 0) break;
|
|
59
|
+
const isLast = i === parts.length - 1;
|
|
60
|
+
const bracketMatch = /^(.+)\[(\d+)\]$/.exec(part);
|
|
61
|
+
if (bracketMatch?.[1] !== void 0 && bracketMatch[2] !== void 0) {
|
|
62
|
+
const key = bracketMatch[1];
|
|
63
|
+
const index = Number(bracketMatch[2]);
|
|
64
|
+
const existing = current[key];
|
|
65
|
+
const arr = Array.isArray(existing) ? existing.slice() : [];
|
|
66
|
+
if (isLast) arr[index] = leafValue;
|
|
67
|
+
current[key] = arr;
|
|
68
|
+
const nextCurrent = arr[index];
|
|
69
|
+
if (nextCurrent !== void 0 && isObject(nextCurrent)) current = nextCurrent;
|
|
70
|
+
} else if (isLast) current[part] = leafValue;
|
|
71
|
+
else {
|
|
72
|
+
const existing = current[part];
|
|
73
|
+
const next = isObject(existing) ? { ...existing } : {};
|
|
74
|
+
current[part] = next;
|
|
75
|
+
current = next;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { resolvePath, resolveValue, setNestedValue };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ReactNode } from "react";
|
|
1
|
+
import { r as ComponentResolver } from "../renderer-BdSqllx5.mjs";
|
|
3
2
|
|
|
4
3
|
//#region src/react/headless.d.ts
|
|
5
|
-
declare function toReactNode(value: unknown): ReactNode;
|
|
6
4
|
/**
|
|
7
5
|
* The headless resolver uses props.renderChild for recursive rendering.
|
|
8
6
|
* No factory function needed — the renderChild is always available
|
|
@@ -10,4 +8,4 @@ declare function toReactNode(value: unknown): ReactNode;
|
|
|
10
8
|
*/
|
|
11
9
|
declare const headlessResolver: ComponentResolver;
|
|
12
10
|
//#endregion
|
|
13
|
-
export { headlessResolver
|
|
11
|
+
export { headlessResolver };
|
package/dist/react/headless.mjs
CHANGED
|
@@ -1,496 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isValidElement, useCallback, useRef } from "react";
|
|
3
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown } from "./headlessRenderers.mjs";
|
|
4
2
|
//#region src/react/headless.tsx
|
|
5
3
|
/**
|
|
6
|
-
* React headless renderer — the default ComponentResolver implementation.
|
|
7
|
-
*
|
|
8
|
-
* Produces plain HTML elements for every schema type. Theme adapters
|
|
9
|
-
* replace this by implementing ComponentResolver with their own components.
|
|
10
|
-
*
|
|
11
|
-
* Accessibility:
|
|
12
|
-
* - All inputs have `id`; labels use `htmlFor` for programmatic association
|
|
13
|
-
* - Discriminated union tabs follow WAI-ARIA tabs pattern (role, aria-selected,
|
|
14
|
-
* arrow key navigation, Home/End)
|
|
15
|
-
* - Checkboxes are linked to visible labels where available
|
|
16
|
-
* - Validation state surfaced via `aria-invalid` and `aria-errormessage`
|
|
17
|
-
*
|
|
18
|
-
* This module imports React and lives in the react layer, not core,
|
|
19
|
-
* because it produces ReactNode values.
|
|
20
|
-
*/
|
|
21
|
-
function toReactNode(value) {
|
|
22
|
-
if (value === null || value === void 0) return null;
|
|
23
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
24
|
-
if (isValidElement(value)) return value;
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
function formatDateTime(value) {
|
|
28
|
-
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
29
|
-
try {
|
|
30
|
-
const date = new Date(value);
|
|
31
|
-
if (isNaN(date.getTime())) return void 0;
|
|
32
|
-
return date.toLocaleString();
|
|
33
|
-
} catch {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function formatDate(value) {
|
|
38
|
-
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
39
|
-
try {
|
|
40
|
-
const date = new Date(value);
|
|
41
|
-
if (isNaN(date.getTime())) return void 0;
|
|
42
|
-
return date.toLocaleDateString();
|
|
43
|
-
} catch {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function formatTime(value) {
|
|
48
|
-
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
49
|
-
try {
|
|
50
|
-
const date = new Date(value);
|
|
51
|
-
if (isNaN(date.getTime())) return void 0;
|
|
52
|
-
return date.toLocaleTimeString();
|
|
53
|
-
} catch {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function dateInputType(format) {
|
|
58
|
-
if (format === "date") return "date";
|
|
59
|
-
if (format === "time") return "time";
|
|
60
|
-
if (format === "date-time" || format === "datetime") return "datetime-local";
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Build a stable, unique-ish input ID from the path.
|
|
64
|
-
* Used for `htmlFor`/`id` association between labels and inputs.
|
|
65
|
-
*/
|
|
66
|
-
function inputId(path) {
|
|
67
|
-
if (path.length === 0) return "sc-field";
|
|
68
|
-
return `sc-${path}`;
|
|
69
|
-
}
|
|
70
|
-
function renderString(props) {
|
|
71
|
-
const id = inputId(props.path);
|
|
72
|
-
if (props.readOnly) {
|
|
73
|
-
const strValue = typeof props.value === "string" ? props.value : void 0;
|
|
74
|
-
if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
75
|
-
id,
|
|
76
|
-
"aria-readonly": "true",
|
|
77
|
-
children: "\\u2014"
|
|
78
|
-
});
|
|
79
|
-
const format = props.constraints.format;
|
|
80
|
-
if (format === "email") return /* @__PURE__ */ jsx("a", {
|
|
81
|
-
href: `mailto:${strValue}`,
|
|
82
|
-
id,
|
|
83
|
-
"aria-readonly": "true",
|
|
84
|
-
children: strValue
|
|
85
|
-
});
|
|
86
|
-
if (format === "uri" || format === "url") return /* @__PURE__ */ jsx("a", {
|
|
87
|
-
href: strValue,
|
|
88
|
-
id,
|
|
89
|
-
"aria-readonly": "true",
|
|
90
|
-
children: strValue
|
|
91
|
-
});
|
|
92
|
-
if (format === "date") return /* @__PURE__ */ jsx("span", {
|
|
93
|
-
id,
|
|
94
|
-
"aria-readonly": "true",
|
|
95
|
-
children: formatDate(strValue) ?? strValue
|
|
96
|
-
});
|
|
97
|
-
if (format === "time") return /* @__PURE__ */ jsx("span", {
|
|
98
|
-
id,
|
|
99
|
-
"aria-readonly": "true",
|
|
100
|
-
children: formatTime(strValue) ?? strValue
|
|
101
|
-
});
|
|
102
|
-
if (format === "date-time" || format === "datetime") return /* @__PURE__ */ jsx("span", {
|
|
103
|
-
id,
|
|
104
|
-
"aria-readonly": "true",
|
|
105
|
-
children: formatDateTime(strValue) ?? strValue
|
|
106
|
-
});
|
|
107
|
-
return /* @__PURE__ */ jsx("span", {
|
|
108
|
-
id,
|
|
109
|
-
"aria-readonly": "true",
|
|
110
|
-
children: strValue
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
const strValue = typeof props.value === "string" ? props.value : "";
|
|
114
|
-
const dateType = dateInputType(props.constraints.format);
|
|
115
|
-
const ariaAttrs = {};
|
|
116
|
-
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
117
|
-
if (dateType !== void 0) return /* @__PURE__ */ jsx("input", {
|
|
118
|
-
id,
|
|
119
|
-
type: dateType,
|
|
120
|
-
value: props.writeOnly ? "" : strValue,
|
|
121
|
-
onChange: (e) => {
|
|
122
|
-
props.onChange(e.target.value);
|
|
123
|
-
},
|
|
124
|
-
...ariaAttrs
|
|
125
|
-
});
|
|
126
|
-
if (props.enumValues !== void 0 && props.enumValues.length > 0) return /* @__PURE__ */ jsxs("select", {
|
|
127
|
-
id,
|
|
128
|
-
value: strValue,
|
|
129
|
-
onChange: (e) => {
|
|
130
|
-
props.onChange(e.target.value);
|
|
131
|
-
},
|
|
132
|
-
...ariaAttrs,
|
|
133
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
134
|
-
value: "",
|
|
135
|
-
children: "Select\\u2026"
|
|
136
|
-
}), props.enumValues.map((v) => /* @__PURE__ */ jsx("option", {
|
|
137
|
-
value: v,
|
|
138
|
-
children: v
|
|
139
|
-
}, v))]
|
|
140
|
-
});
|
|
141
|
-
return /* @__PURE__ */ jsx("input", {
|
|
142
|
-
id,
|
|
143
|
-
type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
|
|
144
|
-
value: props.writeOnly ? "" : strValue,
|
|
145
|
-
onChange: (e) => {
|
|
146
|
-
props.onChange(e.target.value);
|
|
147
|
-
},
|
|
148
|
-
placeholder: typeof props.meta.description === "string" ? props.meta.description : void 0,
|
|
149
|
-
minLength: props.constraints.minLength,
|
|
150
|
-
maxLength: props.constraints.maxLength,
|
|
151
|
-
...ariaAttrs
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
function renderNumber(props) {
|
|
155
|
-
const id = inputId(props.path);
|
|
156
|
-
if (props.readOnly) {
|
|
157
|
-
if (typeof props.value !== "number") return /* @__PURE__ */ jsx("span", {
|
|
158
|
-
id,
|
|
159
|
-
"aria-readonly": "true",
|
|
160
|
-
children: "\\u2014"
|
|
161
|
-
});
|
|
162
|
-
return /* @__PURE__ */ jsx("span", {
|
|
163
|
-
id,
|
|
164
|
-
"aria-readonly": "true",
|
|
165
|
-
children: props.value.toLocaleString()
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const numValue = typeof props.value === "number" ? props.value : "";
|
|
169
|
-
const ariaAttrs = {};
|
|
170
|
-
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
171
|
-
return /* @__PURE__ */ jsx("input", {
|
|
172
|
-
id,
|
|
173
|
-
type: "number",
|
|
174
|
-
value: props.writeOnly ? "" : numValue,
|
|
175
|
-
onChange: (e) => {
|
|
176
|
-
props.onChange(Number(e.target.value));
|
|
177
|
-
},
|
|
178
|
-
min: props.constraints.minimum,
|
|
179
|
-
max: props.constraints.maximum,
|
|
180
|
-
...ariaAttrs
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
function renderBoolean(props) {
|
|
184
|
-
const id = inputId(props.path);
|
|
185
|
-
if (props.readOnly) {
|
|
186
|
-
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx("span", {
|
|
187
|
-
id,
|
|
188
|
-
"aria-readonly": "true",
|
|
189
|
-
children: "\\u2014"
|
|
190
|
-
});
|
|
191
|
-
return /* @__PURE__ */ jsx("span", {
|
|
192
|
-
id,
|
|
193
|
-
"aria-readonly": "true",
|
|
194
|
-
children: props.value ? "Yes" : "No"
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
const ariaAttrs = {};
|
|
198
|
-
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
199
|
-
if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
|
|
200
|
-
return /* @__PURE__ */ jsx("input", {
|
|
201
|
-
id,
|
|
202
|
-
type: "checkbox",
|
|
203
|
-
checked: props.writeOnly ? false : props.value === true,
|
|
204
|
-
onChange: (e) => {
|
|
205
|
-
props.onChange(e.target.checked);
|
|
206
|
-
},
|
|
207
|
-
...ariaAttrs
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
function renderEnum(props) {
|
|
211
|
-
const id = inputId(props.path);
|
|
212
|
-
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
213
|
-
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
214
|
-
id,
|
|
215
|
-
"aria-readonly": "true",
|
|
216
|
-
children: enumValue || "—"
|
|
217
|
-
});
|
|
218
|
-
const ariaAttrs = {};
|
|
219
|
-
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
220
|
-
return /* @__PURE__ */ jsxs("select", {
|
|
221
|
-
id,
|
|
222
|
-
value: props.writeOnly ? "" : enumValue,
|
|
223
|
-
onChange: (e) => {
|
|
224
|
-
props.onChange(e.target.value);
|
|
225
|
-
},
|
|
226
|
-
...ariaAttrs,
|
|
227
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
228
|
-
value: "",
|
|
229
|
-
children: "Select\\u2026"
|
|
230
|
-
}), props.enumValues?.map((v) => /* @__PURE__ */ jsx("option", {
|
|
231
|
-
value: v,
|
|
232
|
-
children: v
|
|
233
|
-
}, v))]
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
function renderObject(props) {
|
|
237
|
-
const obj = isObject(props.value) ? props.value : {};
|
|
238
|
-
const fields = props.fields;
|
|
239
|
-
if (fields === void 0) return null;
|
|
240
|
-
const sortedEntries = Object.entries(fields).sort((a, b) => {
|
|
241
|
-
return (typeof a[1].meta.order === "number" ? a[1].meta.order : Infinity) - (typeof b[1].meta.order === "number" ? b[1].meta.order : Infinity);
|
|
242
|
-
});
|
|
243
|
-
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]) => {
|
|
244
|
-
const childValue = obj[key];
|
|
245
|
-
const childId = inputId(props.path ? `${props.path}.${key}` : key);
|
|
246
|
-
const childOnChange = (v) => {
|
|
247
|
-
const updated = {};
|
|
248
|
-
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
249
|
-
updated[key] = v;
|
|
250
|
-
props.onChange(updated);
|
|
251
|
-
};
|
|
252
|
-
const child = toReactNode(props.renderChild(field, childValue, childOnChange));
|
|
253
|
-
if (child === null || child === void 0) return null;
|
|
254
|
-
return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsxs("label", {
|
|
255
|
-
htmlFor: childId,
|
|
256
|
-
children: [field.meta.description, field.isOptional === false && /* @__PURE__ */ jsxs("span", {
|
|
257
|
-
"aria-hidden": "true",
|
|
258
|
-
style: { color: "#dc2626" },
|
|
259
|
-
children: [" ", "*"]
|
|
260
|
-
})]
|
|
261
|
-
}), child] }, key);
|
|
262
|
-
})] });
|
|
263
|
-
}
|
|
264
|
-
function renderRecord(props) {
|
|
265
|
-
const obj = isObject(props.value) ? props.value : {};
|
|
266
|
-
const valueType = props.valueType;
|
|
267
|
-
if (valueType === void 0) return null;
|
|
268
|
-
const entries = Object.entries(obj);
|
|
269
|
-
if (entries.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
270
|
-
"aria-readonly": props.readOnly ? "true" : void 0,
|
|
271
|
-
children: "—"
|
|
272
|
-
});
|
|
273
|
-
return /* @__PURE__ */ jsx("div", {
|
|
274
|
-
role: "group",
|
|
275
|
-
"aria-label": props.meta.description ?? "Record",
|
|
276
|
-
children: entries.map(([key, value]) => {
|
|
277
|
-
const childId = inputId(props.path ? `${props.path}.${key}` : key);
|
|
278
|
-
const childOnChange = (nextValue) => {
|
|
279
|
-
const updated = {};
|
|
280
|
-
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
281
|
-
updated[key] = nextValue;
|
|
282
|
-
props.onChange(updated);
|
|
283
|
-
};
|
|
284
|
-
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
285
|
-
htmlFor: childId,
|
|
286
|
-
children: key
|
|
287
|
-
}), toReactNode(props.renderChild(valueType, value, childOnChange))] }, key);
|
|
288
|
-
})
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
function renderArray(props) {
|
|
292
|
-
const arr = Array.isArray(props.value) ? props.value : [];
|
|
293
|
-
const element = props.element;
|
|
294
|
-
if (element === void 0) return null;
|
|
295
|
-
if (arr.length === 0) return null;
|
|
296
|
-
return /* @__PURE__ */ jsx("div", {
|
|
297
|
-
role: "group",
|
|
298
|
-
"aria-label": props.meta.description ?? void 0,
|
|
299
|
-
children: arr.map((item, i) => {
|
|
300
|
-
const childOnChange = (v) => {
|
|
301
|
-
const next = arr.slice();
|
|
302
|
-
next[i] = v;
|
|
303
|
-
props.onChange(next);
|
|
304
|
-
};
|
|
305
|
-
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
306
|
-
})
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
function renderUnion(props) {
|
|
310
|
-
const options = props.options;
|
|
311
|
-
if (options === void 0 || options.length === 0) {
|
|
312
|
-
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
313
|
-
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
314
|
-
}
|
|
315
|
-
const matched = matchUnionOption(options, props.value);
|
|
316
|
-
if (matched !== void 0) return toReactNode(props.renderChild(matched, props.value, props.onChange));
|
|
317
|
-
const firstOption = options[0];
|
|
318
|
-
if (firstOption !== void 0) return toReactNode(props.renderChild(firstOption, props.value, props.onChange));
|
|
319
|
-
return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
320
|
-
}
|
|
321
|
-
function renderDiscriminatedUnion(props) {
|
|
322
|
-
const options = props.options;
|
|
323
|
-
const discriminator = props.discriminator;
|
|
324
|
-
if (options === void 0 || options.length === 0) {
|
|
325
|
-
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
326
|
-
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
327
|
-
}
|
|
328
|
-
const obj = isObject(props.value) ? props.value : {};
|
|
329
|
-
const discKey = discriminator ?? "";
|
|
330
|
-
const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
|
|
331
|
-
const optionLabels = options.map((opt) => {
|
|
332
|
-
const discriminatorField = opt.fields?.[discKey];
|
|
333
|
-
if (discriminatorField !== void 0) {
|
|
334
|
-
const constVal = discriminatorField.literalValues?.[0];
|
|
335
|
-
if (typeof constVal === "string") return constVal;
|
|
336
|
-
}
|
|
337
|
-
return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
|
|
338
|
-
});
|
|
339
|
-
let activeIndex = 0;
|
|
340
|
-
if (currentDiscriminatorValue !== void 0) {
|
|
341
|
-
const found = optionLabels.indexOf(currentDiscriminatorValue);
|
|
342
|
-
if (found !== -1) activeIndex = found;
|
|
343
|
-
}
|
|
344
|
-
const activeOption = options[activeIndex];
|
|
345
|
-
const panelId = inputId(props.path);
|
|
346
|
-
if (props.readOnly) {
|
|
347
|
-
if (activeOption !== void 0) return toReactNode(props.renderChild(activeOption, props.value, props.onChange));
|
|
348
|
-
return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
349
|
-
}
|
|
350
|
-
return /* @__PURE__ */ jsx(DiscriminatedUnionTabs, {
|
|
351
|
-
options,
|
|
352
|
-
optionLabels,
|
|
353
|
-
activeIndex,
|
|
354
|
-
panelId,
|
|
355
|
-
discKey,
|
|
356
|
-
props
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* WAI-ARIA tabs component for discriminated unions.
|
|
361
|
-
* Implements the full tabs keyboard pattern:
|
|
362
|
-
* - Left/Right arrow keys move between tabs
|
|
363
|
-
* - Home/End move to first/last tab
|
|
364
|
-
* - Tab moves focus into the active panel
|
|
365
|
-
* - aria-selected, aria-controls, role="tablist"/"tab"/"tabpanel"
|
|
366
|
-
*/
|
|
367
|
-
function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
|
|
368
|
-
const tabRefs = useRef([]);
|
|
369
|
-
const handleTabChange = useCallback((newIndex) => {
|
|
370
|
-
const label = optionLabels[newIndex];
|
|
371
|
-
if (label === void 0) return;
|
|
372
|
-
props.onChange({ [discKey]: label });
|
|
373
|
-
}, [
|
|
374
|
-
optionLabels,
|
|
375
|
-
discKey,
|
|
376
|
-
props
|
|
377
|
-
]);
|
|
378
|
-
const focusTab = useCallback((index) => {
|
|
379
|
-
const clamped = (index % options.length + options.length) % options.length;
|
|
380
|
-
tabRefs.current[clamped]?.focus();
|
|
381
|
-
}, [options.length]);
|
|
382
|
-
const handleKeyDown = useCallback((e) => {
|
|
383
|
-
if (e.key === "ArrowRight") {
|
|
384
|
-
e.preventDefault();
|
|
385
|
-
focusTab(activeIndex + 1);
|
|
386
|
-
} else if (e.key === "ArrowLeft") {
|
|
387
|
-
e.preventDefault();
|
|
388
|
-
focusTab(activeIndex - 1);
|
|
389
|
-
} else if (e.key === "Home") {
|
|
390
|
-
e.preventDefault();
|
|
391
|
-
focusTab(0);
|
|
392
|
-
} else if (e.key === "End") {
|
|
393
|
-
e.preventDefault();
|
|
394
|
-
focusTab(options.length - 1);
|
|
395
|
-
}
|
|
396
|
-
}, [
|
|
397
|
-
activeIndex,
|
|
398
|
-
focusTab,
|
|
399
|
-
options.length
|
|
400
|
-
]);
|
|
401
|
-
const activeOption = options[activeIndex];
|
|
402
|
-
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
403
|
-
role: "tablist",
|
|
404
|
-
"aria-label": "Select variant",
|
|
405
|
-
style: {
|
|
406
|
-
display: "flex",
|
|
407
|
-
gap: "0.25rem",
|
|
408
|
-
marginBottom: "0.5rem"
|
|
409
|
-
},
|
|
410
|
-
onKeyDown: handleKeyDown,
|
|
411
|
-
children: options.map((_opt, i) => /* @__PURE__ */ jsx("button", {
|
|
412
|
-
ref: (el) => {
|
|
413
|
-
tabRefs.current[i] = el;
|
|
414
|
-
},
|
|
415
|
-
type: "button",
|
|
416
|
-
role: "tab",
|
|
417
|
-
"aria-selected": i === activeIndex ? "true" : void 0,
|
|
418
|
-
"aria-controls": `${panelId}-panel`,
|
|
419
|
-
tabIndex: i === activeIndex ? 0 : -1,
|
|
420
|
-
onClick: () => {
|
|
421
|
-
handleTabChange(i);
|
|
422
|
-
},
|
|
423
|
-
style: {
|
|
424
|
-
padding: "0.25rem 0.75rem",
|
|
425
|
-
border: i === activeIndex ? "1px solid #3b82f6" : "1px solid #d1d5db",
|
|
426
|
-
borderRadius: "0.25rem",
|
|
427
|
-
background: i === activeIndex ? "#eff6ff" : "transparent",
|
|
428
|
-
cursor: "pointer",
|
|
429
|
-
fontSize: "0.875rem"
|
|
430
|
-
},
|
|
431
|
-
children: optionLabels[i]
|
|
432
|
-
}, String(i)))
|
|
433
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
434
|
-
role: "tabpanel",
|
|
435
|
-
id: `${panelId}-panel`,
|
|
436
|
-
"aria-labelledby": `${panelId}-tab-${String(activeIndex)}`,
|
|
437
|
-
children: activeOption !== void 0 && toReactNode(props.renderChild(activeOption, props.value, props.onChange))
|
|
438
|
-
})] });
|
|
439
|
-
}
|
|
440
|
-
function renderFile(props) {
|
|
441
|
-
const id = inputId(props.path);
|
|
442
|
-
const accept = props.constraints.mimeTypes?.join(",");
|
|
443
|
-
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
444
|
-
id,
|
|
445
|
-
"aria-readonly": "true",
|
|
446
|
-
children: "File field"
|
|
447
|
-
});
|
|
448
|
-
const ariaAttrs = {};
|
|
449
|
-
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
450
|
-
if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
|
|
451
|
-
return /* @__PURE__ */ jsx("input", {
|
|
452
|
-
id,
|
|
453
|
-
type: "file",
|
|
454
|
-
accept,
|
|
455
|
-
onChange: (e) => {
|
|
456
|
-
const file = e.target.files?.[0];
|
|
457
|
-
if (file !== void 0) props.onChange(file);
|
|
458
|
-
},
|
|
459
|
-
...ariaAttrs
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
function renderUnknown(props) {
|
|
463
|
-
const id = inputId(props.path);
|
|
464
|
-
if (props.readOnly) {
|
|
465
|
-
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", {
|
|
466
|
-
id,
|
|
467
|
-
"aria-readonly": "true",
|
|
468
|
-
children: "\\u2014"
|
|
469
|
-
});
|
|
470
|
-
return /* @__PURE__ */ jsx("span", {
|
|
471
|
-
id,
|
|
472
|
-
"aria-readonly": "true",
|
|
473
|
-
children: typeof props.value === "string" ? props.value : JSON.stringify(props.value)
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
const strValue = typeof props.value === "string" ? props.value : "";
|
|
477
|
-
return /* @__PURE__ */ jsx("input", {
|
|
478
|
-
id,
|
|
479
|
-
type: "text",
|
|
480
|
-
value: props.writeOnly ? "" : strValue,
|
|
481
|
-
onChange: (e) => {
|
|
482
|
-
props.onChange(e.target.value);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
function matchUnionOption(options, value) {
|
|
487
|
-
if (typeof value === "string") return options.find((o) => o.type === "string" || o.type === "enum");
|
|
488
|
-
if (typeof value === "number") return options.find((o) => o.type === "number");
|
|
489
|
-
if (typeof value === "boolean") return options.find((o) => o.type === "boolean");
|
|
490
|
-
if (Array.isArray(value)) return options.find((o) => o.type === "array");
|
|
491
|
-
if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
4
|
* The headless resolver uses props.renderChild for recursive rendering.
|
|
495
5
|
* No factory function needed — the renderChild is always available
|
|
496
6
|
* on RenderProps.
|
|
@@ -506,7 +16,8 @@ const headlessResolver = {
|
|
|
506
16
|
union: renderUnion,
|
|
507
17
|
discriminatedUnion: renderDiscriminatedUnion,
|
|
508
18
|
file: renderFile,
|
|
19
|
+
recursive: renderRecursive,
|
|
509
20
|
unknown: renderUnknown
|
|
510
21
|
};
|
|
511
22
|
//#endregion
|
|
512
|
-
export { headlessResolver
|
|
23
|
+
export { headlessResolver };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { l as RenderProps } from "../renderer-BdSqllx5.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/headlessRenderers.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Coerce an unknown render result into a React node.
|
|
7
|
+
* Returns `null` for unrecognised values.
|
|
8
|
+
*/
|
|
9
|
+
declare function toReactNode(value: unknown): ReactNode;
|
|
10
|
+
declare function renderString(props: RenderProps): ReactNode;
|
|
11
|
+
declare function renderNumber(props: RenderProps): ReactNode;
|
|
12
|
+
declare function renderBoolean(props: RenderProps): ReactNode;
|
|
13
|
+
declare function renderEnum(props: RenderProps): ReactNode;
|
|
14
|
+
declare function renderObject(props: RenderProps): ReactNode;
|
|
15
|
+
declare function renderRecord(props: RenderProps): ReactNode;
|
|
16
|
+
declare function renderArray(props: RenderProps): ReactNode;
|
|
17
|
+
declare function renderUnion(props: RenderProps): ReactNode;
|
|
18
|
+
declare function renderDiscriminatedUnion(props: RenderProps): ReactNode;
|
|
19
|
+
declare function renderFile(props: RenderProps): ReactNode;
|
|
20
|
+
declare function renderRecursive(props: RenderProps): ReactNode;
|
|
21
|
+
declare function renderUnknown(props: RenderProps): ReactNode;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|