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,507 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement, useCallback, useRef } from "react";
|
|
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,44 @@
|
|
|
1
|
+
import { i as DiagnosticsOptions } from "./diagnostics-DzbZmcLI.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/core/ref.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolver function for external $ref URIs.
|
|
6
|
+
* Called with the URI portion (everything before `#`) of an external ref.
|
|
7
|
+
* Returns the parsed document (JSON object) or undefined.
|
|
8
|
+
*/
|
|
9
|
+
type ExternalResolver = (uri: string) => unknown;
|
|
10
|
+
/**
|
|
11
|
+
* Options for $ref resolution.
|
|
12
|
+
*/
|
|
13
|
+
interface RefOptions {
|
|
14
|
+
diagnostics?: DiagnosticsOptions;
|
|
15
|
+
externalResolver?: ExternalResolver;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Count all distinct `$ref` strings reachable from a root document.
|
|
19
|
+
* A chain longer than the number of distinct refs is necessarily cyclic.
|
|
20
|
+
* Returns at least 1 so that single-ref schemas have a usable bound.
|
|
21
|
+
*/
|
|
22
|
+
declare function countDistinctRefs(root: Record<string, unknown>): number;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a `$ref` in a schema against a root document.
|
|
25
|
+
* Returns the original schema if no `$ref` is present.
|
|
26
|
+
* Returns an unknown-schema placeholder on cycle or depth exceeded.
|
|
27
|
+
*
|
|
28
|
+
* The depth bound is derived from the number of distinct `$ref` strings
|
|
29
|
+
* in the root document — a chain longer than that count is necessarily
|
|
30
|
+
* cyclic. When `maxDepth` is not provided, a reasonable default is used.
|
|
31
|
+
*/
|
|
32
|
+
declare function resolveRef(schema: Record<string, unknown>, rootDocument: Record<string, unknown>, visited: Set<string>, diagnostics?: DiagnosticsOptions, maxDepth?: number, externalResolver?: ExternalResolver): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* Dereference a JSON Pointer fragment (`#/path/to/schema`) or an
|
|
35
|
+
* `$anchor` (`#SomeName`) against a root document.
|
|
36
|
+
*/
|
|
37
|
+
declare function dereference(ref: string, root: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Recursively scan a schema document for a `$anchor` matching the given name.
|
|
40
|
+
* Returns the schema object containing the anchor, or undefined.
|
|
41
|
+
*/
|
|
42
|
+
declare function findAnchor(node: unknown, anchorName: string): Record<string, unknown> | undefined;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { findAnchor as a, dereference as i, RefOptions as n, resolveRef as o, countDistinctRefs as r, ExternalResolver as t };
|