schema-components 1.12.10 → 1.13.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 +31 -0
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +37 -8
- package/dist/core/constraints.d.mts +16 -0
- package/dist/core/constraints.mjs +138 -0
- package/dist/core/merge.d.mts +32 -0
- package/dist/core/merge.mjs +96 -0
- package/dist/core/normalise.d.mts +40 -0
- package/dist/core/normalise.mjs +171 -0
- package/dist/core/openapi30.d.mts +38 -0
- package/dist/core/openapi30.mjs +223 -0
- package/dist/core/ref.d.mts +25 -0
- package/dist/core/ref.mjs +86 -0
- package/dist/core/renderer.d.mts +2 -2
- package/dist/core/renderer.mjs +8 -0
- package/dist/core/swagger2.d.mts +10 -0
- package/dist/core/swagger2.mjs +294 -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 +55 -2
- package/dist/core/version.d.mts +2 -0
- package/dist/core/version.mjs +79 -0
- package/dist/core/walkBuilders.d.mts +52 -0
- package/dist/core/walkBuilders.mjs +152 -0
- package/dist/core/walker.d.mts +3 -10
- package/dist/core/walker.mjs +143 -231
- 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/openapi/components.d.mts +2 -1
- package/dist/openapi/parser.d.mts +59 -2
- package/dist/openapi/parser.mjs +189 -8
- package/dist/react/SchemaComponent.d.mts +4 -2
- package/dist/react/SchemaComponent.mjs +39 -85
- package/dist/react/SchemaView.d.mts +2 -1
- package/dist/react/SchemaView.mjs +21 -9
- 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/renderer-DseHeliw.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 +2 -1
- 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-CRPqVwKu.d.mts +299 -0
- package/dist/types-ag2jYLqQ.d.mts +261 -0
- package/dist/version-CLchheaH.d.mts +40 -0
- package/package.json +1 -1
- package/dist/types-BJzEgJdX.d.mts +0 -335
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { isValidElement, useCallback, useRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/headlessRenderers.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Headless renderer functions — one per schema type.
|
|
7
|
+
*
|
|
8
|
+
* Produces plain React elements for every schema type. These functions
|
|
9
|
+
* are composed into `headlessResolver` by `headless.tsx`.
|
|
10
|
+
*
|
|
11
|
+
* This module contains the individual render functions, date/time helpers,
|
|
12
|
+
* ID generation, union matching, and the discriminated union tabs component.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Coerce an unknown render result into a React node.
|
|
16
|
+
* Returns `null` for unrecognised values.
|
|
17
|
+
*/
|
|
18
|
+
function toReactNode(value) {
|
|
19
|
+
if (value === null || value === void 0) return null;
|
|
20
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
21
|
+
if (isValidElement(value)) return value;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function formatDateTime(value) {
|
|
25
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
26
|
+
try {
|
|
27
|
+
const date = new Date(value);
|
|
28
|
+
if (isNaN(date.getTime())) return void 0;
|
|
29
|
+
return date.toLocaleString();
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function formatDate(value) {
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
36
|
+
try {
|
|
37
|
+
const date = new Date(value);
|
|
38
|
+
if (isNaN(date.getTime())) return void 0;
|
|
39
|
+
return date.toLocaleDateString();
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function formatTime(value) {
|
|
45
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
46
|
+
try {
|
|
47
|
+
const date = new Date(value);
|
|
48
|
+
if (isNaN(date.getTime())) return void 0;
|
|
49
|
+
return date.toLocaleTimeString();
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function dateInputType(format) {
|
|
55
|
+
if (format === "date") return "date";
|
|
56
|
+
if (format === "time") return "time";
|
|
57
|
+
if (format === "date-time" || format === "datetime") return "datetime-local";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a stable, unique-ish input ID from the path.
|
|
61
|
+
* Used for `htmlFor`/`id` association between labels and inputs.
|
|
62
|
+
*/
|
|
63
|
+
function inputId(path) {
|
|
64
|
+
if (path.length === 0) return "sc-field";
|
|
65
|
+
return `sc-${path}`;
|
|
66
|
+
}
|
|
67
|
+
function renderString(props) {
|
|
68
|
+
const id = inputId(props.path);
|
|
69
|
+
if (props.readOnly) {
|
|
70
|
+
const strValue = typeof props.value === "string" ? props.value : void 0;
|
|
71
|
+
if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
72
|
+
id,
|
|
73
|
+
"aria-readonly": "true",
|
|
74
|
+
children: "\\u2014"
|
|
75
|
+
});
|
|
76
|
+
const format = props.constraints.format;
|
|
77
|
+
if (format === "email") return /* @__PURE__ */ jsx("a", {
|
|
78
|
+
href: `mailto:${strValue}`,
|
|
79
|
+
id,
|
|
80
|
+
"aria-readonly": "true",
|
|
81
|
+
children: strValue
|
|
82
|
+
});
|
|
83
|
+
if (format === "uri" || format === "url") return /* @__PURE__ */ jsx("a", {
|
|
84
|
+
href: strValue,
|
|
85
|
+
id,
|
|
86
|
+
"aria-readonly": "true",
|
|
87
|
+
children: strValue
|
|
88
|
+
});
|
|
89
|
+
if (format === "date") return /* @__PURE__ */ jsx("span", {
|
|
90
|
+
id,
|
|
91
|
+
"aria-readonly": "true",
|
|
92
|
+
children: formatDate(strValue) ?? strValue
|
|
93
|
+
});
|
|
94
|
+
if (format === "time") return /* @__PURE__ */ jsx("span", {
|
|
95
|
+
id,
|
|
96
|
+
"aria-readonly": "true",
|
|
97
|
+
children: formatTime(strValue) ?? strValue
|
|
98
|
+
});
|
|
99
|
+
if (format === "date-time" || format === "datetime") return /* @__PURE__ */ jsx("span", {
|
|
100
|
+
id,
|
|
101
|
+
"aria-readonly": "true",
|
|
102
|
+
children: formatDateTime(strValue) ?? strValue
|
|
103
|
+
});
|
|
104
|
+
return /* @__PURE__ */ jsx("span", {
|
|
105
|
+
id,
|
|
106
|
+
"aria-readonly": "true",
|
|
107
|
+
children: strValue
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
111
|
+
const dateType = dateInputType(props.constraints.format);
|
|
112
|
+
const ariaAttrs = {};
|
|
113
|
+
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
114
|
+
if (dateType !== void 0) return /* @__PURE__ */ jsx("input", {
|
|
115
|
+
id,
|
|
116
|
+
type: dateType,
|
|
117
|
+
value: props.writeOnly ? "" : strValue,
|
|
118
|
+
onChange: (e) => {
|
|
119
|
+
props.onChange(e.target.value);
|
|
120
|
+
},
|
|
121
|
+
...ariaAttrs
|
|
122
|
+
});
|
|
123
|
+
if (props.enumValues !== void 0 && props.enumValues.length > 0) return /* @__PURE__ */ jsxs("select", {
|
|
124
|
+
id,
|
|
125
|
+
value: strValue,
|
|
126
|
+
onChange: (e) => {
|
|
127
|
+
props.onChange(e.target.value);
|
|
128
|
+
},
|
|
129
|
+
...ariaAttrs,
|
|
130
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
131
|
+
value: "",
|
|
132
|
+
children: "Select\\u2026"
|
|
133
|
+
}), props.enumValues.map((v) => {
|
|
134
|
+
const display = v === null ? "null" : typeof v === "string" ? v : String(v);
|
|
135
|
+
return /* @__PURE__ */ jsx("option", {
|
|
136
|
+
value: display,
|
|
137
|
+
children: display
|
|
138
|
+
}, display);
|
|
139
|
+
})]
|
|
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) => {
|
|
231
|
+
const display = v === null ? "null" : typeof v === "string" ? v : String(v);
|
|
232
|
+
return /* @__PURE__ */ jsx("option", {
|
|
233
|
+
value: display,
|
|
234
|
+
children: display
|
|
235
|
+
}, display);
|
|
236
|
+
})]
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function renderObject(props) {
|
|
240
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
241
|
+
const fields = props.fields;
|
|
242
|
+
if (fields === void 0) return null;
|
|
243
|
+
const sortedEntries = Object.entries(fields).sort((a, b) => {
|
|
244
|
+
return (typeof a[1].meta.order === "number" ? a[1].meta.order : Infinity) - (typeof b[1].meta.order === "number" ? b[1].meta.order : Infinity);
|
|
245
|
+
});
|
|
246
|
+
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
|
+
const childValue = obj[key];
|
|
248
|
+
const childId = inputId(props.path ? `${props.path}.${key}` : key);
|
|
249
|
+
const childOnChange = (v) => {
|
|
250
|
+
const updated = {};
|
|
251
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
252
|
+
updated[key] = v;
|
|
253
|
+
props.onChange(updated);
|
|
254
|
+
};
|
|
255
|
+
const child = toReactNode(props.renderChild(field, childValue, childOnChange));
|
|
256
|
+
if (child === null || child === void 0) return null;
|
|
257
|
+
return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsxs("label", {
|
|
258
|
+
htmlFor: childId,
|
|
259
|
+
children: [field.meta.description, field.isOptional === false && /* @__PURE__ */ jsxs("span", {
|
|
260
|
+
"aria-hidden": "true",
|
|
261
|
+
style: { color: "#dc2626" },
|
|
262
|
+
children: [" ", "*"]
|
|
263
|
+
})]
|
|
264
|
+
}), child] }, key);
|
|
265
|
+
})] });
|
|
266
|
+
}
|
|
267
|
+
function renderRecord(props) {
|
|
268
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
269
|
+
const valueType = props.valueType;
|
|
270
|
+
if (valueType === void 0) return null;
|
|
271
|
+
const entries = Object.entries(obj);
|
|
272
|
+
if (entries.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
273
|
+
"aria-readonly": props.readOnly ? "true" : void 0,
|
|
274
|
+
children: "—"
|
|
275
|
+
});
|
|
276
|
+
return /* @__PURE__ */ jsx("div", {
|
|
277
|
+
role: "group",
|
|
278
|
+
"aria-label": props.meta.description ?? "Record",
|
|
279
|
+
children: entries.map(([key, value]) => {
|
|
280
|
+
const childId = inputId(props.path ? `${props.path}.${key}` : key);
|
|
281
|
+
const childOnChange = (nextValue) => {
|
|
282
|
+
const updated = {};
|
|
283
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
284
|
+
updated[key] = nextValue;
|
|
285
|
+
props.onChange(updated);
|
|
286
|
+
};
|
|
287
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
288
|
+
htmlFor: childId,
|
|
289
|
+
children: key
|
|
290
|
+
}), toReactNode(props.renderChild(valueType, value, childOnChange))] }, key);
|
|
291
|
+
})
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function renderArray(props) {
|
|
295
|
+
const arr = Array.isArray(props.value) ? props.value : [];
|
|
296
|
+
const element = props.element;
|
|
297
|
+
if (element === void 0) return null;
|
|
298
|
+
if (arr.length === 0) return null;
|
|
299
|
+
return /* @__PURE__ */ jsx("div", {
|
|
300
|
+
role: "group",
|
|
301
|
+
"aria-label": props.meta.description ?? void 0,
|
|
302
|
+
children: arr.map((item, i) => {
|
|
303
|
+
const childOnChange = (v) => {
|
|
304
|
+
const next = arr.slice();
|
|
305
|
+
next[i] = v;
|
|
306
|
+
props.onChange(next);
|
|
307
|
+
};
|
|
308
|
+
return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
|
|
309
|
+
})
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function renderUnion(props) {
|
|
313
|
+
const options = props.options;
|
|
314
|
+
if (options === void 0 || options.length === 0) {
|
|
315
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
316
|
+
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
317
|
+
}
|
|
318
|
+
const matched = matchUnionOption(options, props.value);
|
|
319
|
+
if (matched !== void 0) return toReactNode(props.renderChild(matched, props.value, props.onChange));
|
|
320
|
+
const firstOption = options[0];
|
|
321
|
+
if (firstOption !== void 0) return toReactNode(props.renderChild(firstOption, props.value, props.onChange));
|
|
322
|
+
return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
323
|
+
}
|
|
324
|
+
function renderDiscriminatedUnion(props) {
|
|
325
|
+
const options = props.options;
|
|
326
|
+
const discriminator = props.discriminator;
|
|
327
|
+
if (options === void 0 || options.length === 0) {
|
|
328
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
329
|
+
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
330
|
+
}
|
|
331
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
332
|
+
const discKey = discriminator ?? "";
|
|
333
|
+
const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
|
|
334
|
+
const optionLabels = options.map((opt) => {
|
|
335
|
+
if (opt.type === "object") {
|
|
336
|
+
const discriminatorField = opt.fields[discKey];
|
|
337
|
+
if (discriminatorField?.type === "literal") {
|
|
338
|
+
const constVal = discriminatorField.literalValues[0];
|
|
339
|
+
if (typeof constVal === "string") return constVal;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
|
|
343
|
+
});
|
|
344
|
+
let activeIndex = 0;
|
|
345
|
+
if (currentDiscriminatorValue !== void 0) {
|
|
346
|
+
const found = optionLabels.indexOf(currentDiscriminatorValue);
|
|
347
|
+
if (found !== -1) activeIndex = found;
|
|
348
|
+
}
|
|
349
|
+
const activeOption = options[activeIndex];
|
|
350
|
+
const panelId = inputId(props.path);
|
|
351
|
+
if (props.readOnly) {
|
|
352
|
+
if (activeOption !== void 0) return toReactNode(props.renderChild(activeOption, props.value, props.onChange));
|
|
353
|
+
return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
|
|
354
|
+
}
|
|
355
|
+
return /* @__PURE__ */ jsx(DiscriminatedUnionTabs, {
|
|
356
|
+
options,
|
|
357
|
+
optionLabels,
|
|
358
|
+
activeIndex,
|
|
359
|
+
panelId,
|
|
360
|
+
discKey,
|
|
361
|
+
props
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* WAI-ARIA tabs component for discriminated unions.
|
|
366
|
+
* Implements the full tabs keyboard pattern:
|
|
367
|
+
* - Left/Right arrow keys move between tabs
|
|
368
|
+
* - Home/End move to first/last tab
|
|
369
|
+
* - Tab moves focus into the active panel
|
|
370
|
+
* - aria-selected, aria-controls, role="tablist"/"tab"/"tabpanel"
|
|
371
|
+
*/
|
|
372
|
+
function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
|
|
373
|
+
const tabRefs = useRef([]);
|
|
374
|
+
const handleTabChange = useCallback((newIndex) => {
|
|
375
|
+
const label = optionLabels[newIndex];
|
|
376
|
+
if (label === void 0) return;
|
|
377
|
+
props.onChange({ [discKey]: label });
|
|
378
|
+
}, [
|
|
379
|
+
optionLabels,
|
|
380
|
+
discKey,
|
|
381
|
+
props
|
|
382
|
+
]);
|
|
383
|
+
const focusTab = useCallback((index) => {
|
|
384
|
+
const clamped = (index % options.length + options.length) % options.length;
|
|
385
|
+
tabRefs.current[clamped]?.focus();
|
|
386
|
+
}, [options.length]);
|
|
387
|
+
const handleKeyDown = useCallback((e) => {
|
|
388
|
+
if (e.key === "ArrowRight") {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
focusTab(activeIndex + 1);
|
|
391
|
+
} else if (e.key === "ArrowLeft") {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
focusTab(activeIndex - 1);
|
|
394
|
+
} else if (e.key === "Home") {
|
|
395
|
+
e.preventDefault();
|
|
396
|
+
focusTab(0);
|
|
397
|
+
} else if (e.key === "End") {
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
focusTab(options.length - 1);
|
|
400
|
+
}
|
|
401
|
+
}, [
|
|
402
|
+
activeIndex,
|
|
403
|
+
focusTab,
|
|
404
|
+
options.length
|
|
405
|
+
]);
|
|
406
|
+
const activeOption = options[activeIndex];
|
|
407
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
408
|
+
role: "tablist",
|
|
409
|
+
"aria-label": "Select variant",
|
|
410
|
+
style: {
|
|
411
|
+
display: "flex",
|
|
412
|
+
gap: "0.25rem",
|
|
413
|
+
marginBottom: "0.5rem"
|
|
414
|
+
},
|
|
415
|
+
onKeyDown: handleKeyDown,
|
|
416
|
+
children: options.map((_opt, i) => /* @__PURE__ */ jsx("button", {
|
|
417
|
+
ref: (el) => {
|
|
418
|
+
tabRefs.current[i] = el;
|
|
419
|
+
},
|
|
420
|
+
type: "button",
|
|
421
|
+
role: "tab",
|
|
422
|
+
"aria-selected": i === activeIndex ? "true" : void 0,
|
|
423
|
+
"aria-controls": `${panelId}-panel`,
|
|
424
|
+
tabIndex: i === activeIndex ? 0 : -1,
|
|
425
|
+
onClick: () => {
|
|
426
|
+
handleTabChange(i);
|
|
427
|
+
},
|
|
428
|
+
style: {
|
|
429
|
+
padding: "0.25rem 0.75rem",
|
|
430
|
+
border: i === activeIndex ? "1px solid #3b82f6" : "1px solid #d1d5db",
|
|
431
|
+
borderRadius: "0.25rem",
|
|
432
|
+
background: i === activeIndex ? "#eff6ff" : "transparent",
|
|
433
|
+
cursor: "pointer",
|
|
434
|
+
fontSize: "0.875rem"
|
|
435
|
+
},
|
|
436
|
+
children: optionLabels[i]
|
|
437
|
+
}, String(i)))
|
|
438
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
439
|
+
role: "tabpanel",
|
|
440
|
+
id: `${panelId}-panel`,
|
|
441
|
+
"aria-labelledby": `${panelId}-tab-${String(activeIndex)}`,
|
|
442
|
+
children: activeOption !== void 0 && toReactNode(props.renderChild(activeOption, props.value, props.onChange))
|
|
443
|
+
})] });
|
|
444
|
+
}
|
|
445
|
+
function renderFile(props) {
|
|
446
|
+
const id = inputId(props.path);
|
|
447
|
+
const accept = props.constraints.mimeTypes?.join(",");
|
|
448
|
+
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
449
|
+
id,
|
|
450
|
+
"aria-readonly": "true",
|
|
451
|
+
children: "File field"
|
|
452
|
+
});
|
|
453
|
+
const ariaAttrs = {};
|
|
454
|
+
if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
|
|
455
|
+
if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
|
|
456
|
+
return /* @__PURE__ */ jsx("input", {
|
|
457
|
+
id,
|
|
458
|
+
type: "file",
|
|
459
|
+
accept,
|
|
460
|
+
onChange: (e) => {
|
|
461
|
+
const file = e.target.files?.[0];
|
|
462
|
+
if (file !== void 0) props.onChange(file);
|
|
463
|
+
},
|
|
464
|
+
...ariaAttrs
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function renderRecursive(props) {
|
|
468
|
+
const refTarget = props.refTarget ?? "";
|
|
469
|
+
return /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsxs("em", { children: [
|
|
470
|
+
"↻ ",
|
|
471
|
+
typeof props.meta.description === "string" ? props.meta.description : refTarget,
|
|
472
|
+
" (recursive)"
|
|
473
|
+
] }) });
|
|
474
|
+
}
|
|
475
|
+
function renderUnknown(props) {
|
|
476
|
+
const id = inputId(props.path);
|
|
477
|
+
if (props.readOnly) {
|
|
478
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", {
|
|
479
|
+
id,
|
|
480
|
+
"aria-readonly": "true",
|
|
481
|
+
children: "\\u2014"
|
|
482
|
+
});
|
|
483
|
+
return /* @__PURE__ */ jsx("span", {
|
|
484
|
+
id,
|
|
485
|
+
"aria-readonly": "true",
|
|
486
|
+
children: typeof props.value === "string" ? props.value : JSON.stringify(props.value)
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
490
|
+
return /* @__PURE__ */ jsx("input", {
|
|
491
|
+
id,
|
|
492
|
+
type: "text",
|
|
493
|
+
value: props.writeOnly ? "" : strValue,
|
|
494
|
+
onChange: (e) => {
|
|
495
|
+
props.onChange(e.target.value);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function matchUnionOption(options, value) {
|
|
500
|
+
if (typeof value === "string") return options.find((o) => o.type === "string" || o.type === "enum");
|
|
501
|
+
if (typeof value === "number") return options.find((o) => o.type === "number");
|
|
502
|
+
if (typeof value === "boolean") return options.find((o) => o.type === "boolean");
|
|
503
|
+
if (Array.isArray(value)) return options.find((o) => o.type === "array");
|
|
504
|
+
if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
export { renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { E as StringConstraints, b as ObjectConstraints, f as FileConstraints, j as WalkedField, t as ArrayConstraints, v as NumberConstraints, w as SchemaMeta } from "./types-ag2jYLqQ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/renderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Flat intersection of all constraint types.
|
|
6
|
+
* Used in renderer props where the render function receives the union
|
|
7
|
+
* but knows (by resolver key) which subset applies.
|
|
8
|
+
*
|
|
9
|
+
* The walker's discriminated WalkedField enforces type-correct constraints
|
|
10
|
+
* at construction time; the renderer consumes them as this flat type.
|
|
11
|
+
*/
|
|
12
|
+
type AllConstraints = StringConstraints & NumberConstraints & ArrayConstraints & ObjectConstraints & FileConstraints;
|
|
13
|
+
/**
|
|
14
|
+
* Properties available on every schema field, regardless of rendering target.
|
|
15
|
+
* Both React and HTML renderers receive these.
|
|
16
|
+
*/
|
|
17
|
+
interface BaseFieldProps {
|
|
18
|
+
/** Current field value. */
|
|
19
|
+
value: unknown;
|
|
20
|
+
/** Whether to render as read-only display. */
|
|
21
|
+
readOnly: boolean;
|
|
22
|
+
/** Whether to render as an empty input. */
|
|
23
|
+
writeOnly: boolean;
|
|
24
|
+
/** Schema metadata for this field. */
|
|
25
|
+
meta: SchemaMeta;
|
|
26
|
+
/** Constraints from schema checks. */
|
|
27
|
+
constraints: AllConstraints;
|
|
28
|
+
/** Dot-separated path from root (e.g. "address.city"). */
|
|
29
|
+
path: string;
|
|
30
|
+
/** For enums: the allowed values. */
|
|
31
|
+
enumValues?: (string | number | boolean | null)[];
|
|
32
|
+
/** For arrays: the element schema. */
|
|
33
|
+
element?: WalkedField;
|
|
34
|
+
/** For tuples: positional element schemas from prefixItems. */
|
|
35
|
+
prefixItems?: WalkedField[];
|
|
36
|
+
/** For conditionals: the if/then/else sub-schemas. */
|
|
37
|
+
ifClause?: WalkedField;
|
|
38
|
+
thenClause?: WalkedField;
|
|
39
|
+
elseClause?: WalkedField;
|
|
40
|
+
/** For negations: the negated sub-schema. */
|
|
41
|
+
negated?: WalkedField;
|
|
42
|
+
/** For recursive fields: the $ref string that would create the cycle. */
|
|
43
|
+
refTarget?: string;
|
|
44
|
+
/** For objects: map of field name → WalkedField. */
|
|
45
|
+
fields?: Record<string, WalkedField>;
|
|
46
|
+
/** For unions: the option schemas. */
|
|
47
|
+
options?: WalkedField[];
|
|
48
|
+
/** For discriminated unions: the discriminator key. */
|
|
49
|
+
discriminator?: string;
|
|
50
|
+
/** For records: key and value schemas. */
|
|
51
|
+
keyType?: WalkedField;
|
|
52
|
+
valueType?: WalkedField;
|
|
53
|
+
/** For literals: the literal value(s). */
|
|
54
|
+
literalValues?: (string | number | boolean | null)[];
|
|
55
|
+
/** Example values from the schema's `examples` keyword. */
|
|
56
|
+
examples?: unknown[];
|
|
57
|
+
/** Walked field tree for recursive rendering. */
|
|
58
|
+
tree: WalkedField;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Props for React render functions. Extends BaseFieldProps with:
|
|
62
|
+
* - `onChange` — callback to propagate value changes back to state
|
|
63
|
+
* - `renderChild` — recursively renders a child field, threading onChange
|
|
64
|
+
*/
|
|
65
|
+
interface RenderProps extends BaseFieldProps {
|
|
66
|
+
/** Callback to update the field value. */
|
|
67
|
+
onChange: (value: unknown) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Render a child field. Theme adapters call this to recursively render
|
|
70
|
+
* nested structures (object fields, array elements, union options).
|
|
71
|
+
* The resolver and rendering context are already wired in.
|
|
72
|
+
*/
|
|
73
|
+
renderChild: (tree: WalkedField, value: unknown, onChange: (v: unknown) => void) => unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Props for HTML render functions. Extends BaseFieldProps with:
|
|
77
|
+
* - `renderChild` — recursively renders a child field to HTML string
|
|
78
|
+
*
|
|
79
|
+
* No `onChange` — HTML rendering is pure output with no event handling.
|
|
80
|
+
*/
|
|
81
|
+
interface HtmlRenderProps extends BaseFieldProps {
|
|
82
|
+
/**
|
|
83
|
+
* Render a child field to an HTML string. Theme adapters call this
|
|
84
|
+
* to recursively render nested structures.
|
|
85
|
+
*
|
|
86
|
+
* @param tree - The walked field tree for the child
|
|
87
|
+
* @param value - The child's current value
|
|
88
|
+
* @param pathSuffix - Path segment from the parent (e.g. "city",
|
|
89
|
+
* "[0]"). When omitted, the child's description is used as fallback.
|
|
90
|
+
*/
|
|
91
|
+
renderChild: (tree: WalkedField, value: unknown, pathSuffix?: string) => string;
|
|
92
|
+
}
|
|
93
|
+
type RenderFunction = (props: RenderProps) => unknown;
|
|
94
|
+
interface ComponentResolver {
|
|
95
|
+
string?: RenderFunction;
|
|
96
|
+
number?: RenderFunction;
|
|
97
|
+
boolean?: RenderFunction;
|
|
98
|
+
enum?: RenderFunction;
|
|
99
|
+
object?: RenderFunction;
|
|
100
|
+
array?: RenderFunction;
|
|
101
|
+
tuple?: RenderFunction;
|
|
102
|
+
record?: RenderFunction;
|
|
103
|
+
union?: RenderFunction;
|
|
104
|
+
discriminatedUnion?: RenderFunction;
|
|
105
|
+
conditional?: RenderFunction;
|
|
106
|
+
negation?: RenderFunction;
|
|
107
|
+
recursive?: RenderFunction;
|
|
108
|
+
literal?: RenderFunction;
|
|
109
|
+
file?: RenderFunction;
|
|
110
|
+
unknown?: RenderFunction;
|
|
111
|
+
}
|
|
112
|
+
/** An HTML render function returns a string. */
|
|
113
|
+
type HtmlRenderFunction = (props: HtmlRenderProps) => string;
|
|
114
|
+
/**
|
|
115
|
+
* HTML resolver — maps schema types to HTML string renderers.
|
|
116
|
+
* Structurally mirrors ComponentResolver but produces strings.
|
|
117
|
+
*/
|
|
118
|
+
interface HtmlResolver {
|
|
119
|
+
string?: HtmlRenderFunction;
|
|
120
|
+
number?: HtmlRenderFunction;
|
|
121
|
+
boolean?: HtmlRenderFunction;
|
|
122
|
+
enum?: HtmlRenderFunction;
|
|
123
|
+
object?: HtmlRenderFunction;
|
|
124
|
+
array?: HtmlRenderFunction;
|
|
125
|
+
tuple?: HtmlRenderFunction;
|
|
126
|
+
record?: HtmlRenderFunction;
|
|
127
|
+
union?: HtmlRenderFunction;
|
|
128
|
+
discriminatedUnion?: HtmlRenderFunction;
|
|
129
|
+
conditional?: HtmlRenderFunction;
|
|
130
|
+
negation?: HtmlRenderFunction;
|
|
131
|
+
recursive?: HtmlRenderFunction;
|
|
132
|
+
literal?: HtmlRenderFunction;
|
|
133
|
+
file?: HtmlRenderFunction;
|
|
134
|
+
unknown?: HtmlRenderFunction;
|
|
135
|
+
}
|
|
136
|
+
declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "enum", "object", "array", "tuple", "record", "union", "discriminatedUnion", "conditional", "negation", "recursive", "literal", "file", "unknown"];
|
|
137
|
+
type ResolverKey = (typeof RESOLVER_KEYS)[number];
|
|
138
|
+
/**
|
|
139
|
+
* Map a schema type to the resolver key that handles it.
|
|
140
|
+
* `discriminatedUnion` → `union`. Unknown types → `unknown`.
|
|
141
|
+
*/
|
|
142
|
+
declare function typeToKey(type: WalkedField["type"]): ResolverKey;
|
|
143
|
+
/**
|
|
144
|
+
* Look up the render function for a schema type in a ComponentResolver.
|
|
145
|
+
*/
|
|
146
|
+
declare function getRenderFunction(type: WalkedField["type"], resolver: ComponentResolver): RenderFunction | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Look up the render function for a schema type in an HtmlResolver.
|
|
149
|
+
*/
|
|
150
|
+
declare function getHtmlRenderFn(type: WalkedField["type"], resolver: HtmlResolver): HtmlRenderFunction | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Merge two ComponentResolvers — user values take priority, fallback fills gaps.
|
|
153
|
+
*/
|
|
154
|
+
declare function mergeResolvers(user: ComponentResolver, fallback: ComponentResolver): ComponentResolver;
|
|
155
|
+
/**
|
|
156
|
+
* Merge two HtmlResolvers — user values take priority, fallback fills gaps.
|
|
157
|
+
*/
|
|
158
|
+
declare function mergeHtmlResolvers(user: HtmlResolver, fallback: HtmlResolver): HtmlResolver;
|
|
159
|
+
//#endregion
|
|
160
|
+
export { HtmlRenderProps as a, RenderFunction as c, getRenderFunction as d, mergeHtmlResolvers as f, HtmlRenderFunction as i, RenderProps as l, typeToKey as m, BaseFieldProps as n, HtmlResolver as o, mergeResolvers as p, ComponentResolver as r, RESOLVER_KEYS as s, AllConstraints as t, getHtmlRenderFn as u };
|
package/dist/themes/mantine.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
|
+
import { headlessResolver } from "../react/headless.mjs";
|
|
3
4
|
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
//#region src/themes/mantine.tsx
|
|
5
6
|
function getLabel(props) {
|
package/dist/themes/mui.d.mts
CHANGED
package/dist/themes/mui.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isObject } from "../core/guards.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { toReactNode } from "../react/headlessRenderers.mjs";
|
|
3
|
+
import { headlessResolver } from "../react/headless.mjs";
|
|
3
4
|
import { isValidElement } from "react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/themes/mui.tsx
|