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.
Files changed (83) hide show
  1. package/README.md +35 -0
  2. package/dist/core/adapter.d.mts +8 -3
  3. package/dist/core/adapter.mjs +58 -14
  4. package/dist/core/constraints.d.mts +17 -0
  5. package/dist/core/constraints.mjs +150 -0
  6. package/dist/core/diagnostics.d.mts +2 -0
  7. package/dist/core/diagnostics.mjs +33 -0
  8. package/dist/core/errors.d.mts +1 -1
  9. package/dist/core/formats.d.mts +33 -0
  10. package/dist/core/formats.mjs +67 -0
  11. package/dist/core/merge.d.mts +48 -0
  12. package/dist/core/merge.mjs +125 -0
  13. package/dist/core/normalise.d.mts +41 -0
  14. package/dist/core/normalise.mjs +2 -0
  15. package/dist/core/openapi30.d.mts +41 -0
  16. package/dist/core/openapi30.mjs +2 -0
  17. package/dist/core/ref.d.mts +2 -0
  18. package/dist/core/ref.mjs +165 -0
  19. package/dist/core/renderer.d.mts +2 -2
  20. package/dist/core/renderer.mjs +8 -0
  21. package/dist/core/swagger2.d.mts +11 -0
  22. package/dist/core/swagger2.mjs +2 -0
  23. package/dist/core/typeInference.d.mts +2 -0
  24. package/dist/core/typeInference.mjs +1 -0
  25. package/dist/core/types.d.mts +2 -3
  26. package/dist/core/types.mjs +58 -2
  27. package/dist/core/version.d.mts +2 -0
  28. package/dist/core/version.mjs +151 -0
  29. package/dist/core/walkBuilders.d.mts +66 -0
  30. package/dist/core/walkBuilders.mjs +152 -0
  31. package/dist/core/walker.d.mts +3 -10
  32. package/dist/core/walker.mjs +245 -233
  33. package/dist/diagnostics-DzbZmcLI.d.mts +64 -0
  34. package/dist/html/a11y.d.mts +5 -4
  35. package/dist/html/renderToHtml.d.mts +3 -3
  36. package/dist/html/renderToHtml.mjs +23 -379
  37. package/dist/html/renderToHtmlStream.d.mts +29 -47
  38. package/dist/html/renderToHtmlStream.mjs +33 -305
  39. package/dist/html/renderers.d.mts +14 -0
  40. package/dist/html/renderers.mjs +406 -0
  41. package/dist/html/streamRenderers.d.mts +13 -0
  42. package/dist/html/streamRenderers.mjs +243 -0
  43. package/dist/normalise-tL9FckAk.mjs +748 -0
  44. package/dist/openapi/ApiCallbacks.d.mts +16 -0
  45. package/dist/openapi/ApiCallbacks.mjs +34 -0
  46. package/dist/openapi/ApiLinks.d.mts +16 -0
  47. package/dist/openapi/ApiLinks.mjs +42 -0
  48. package/dist/openapi/ApiResponseHeaders.d.mts +16 -0
  49. package/dist/openapi/ApiResponseHeaders.mjs +35 -0
  50. package/dist/openapi/ApiSecurity.d.mts +19 -0
  51. package/dist/openapi/ApiSecurity.mjs +33 -0
  52. package/dist/openapi/bundle.d.mts +47 -0
  53. package/dist/openapi/bundle.mjs +95 -0
  54. package/dist/openapi/components.d.mts +7 -1
  55. package/dist/openapi/components.mjs +30 -6
  56. package/dist/openapi/parser.d.mts +59 -2
  57. package/dist/openapi/parser.mjs +189 -8
  58. package/dist/react/SchemaComponent.d.mts +13 -4
  59. package/dist/react/SchemaComponent.mjs +51 -91
  60. package/dist/react/SchemaView.d.mts +10 -2
  61. package/dist/react/SchemaView.mjs +33 -15
  62. package/dist/react/fieldPath.d.mts +20 -0
  63. package/dist/react/fieldPath.mjs +81 -0
  64. package/dist/react/headless.d.mts +2 -4
  65. package/dist/react/headless.mjs +3 -492
  66. package/dist/react/headlessRenderers.d.mts +23 -0
  67. package/dist/react/headlessRenderers.mjs +507 -0
  68. package/dist/ref-DvWoULcy.d.mts +44 -0
  69. package/dist/renderer-BdSqllx5.d.mts +160 -0
  70. package/dist/themes/mantine.d.mts +1 -1
  71. package/dist/themes/mantine.mjs +2 -1
  72. package/dist/themes/mui.d.mts +1 -1
  73. package/dist/themes/mui.mjs +3 -2
  74. package/dist/themes/radix.d.mts +1 -1
  75. package/dist/themes/radix.mjs +2 -1
  76. package/dist/themes/shadcn.d.mts +1 -1
  77. package/dist/themes/shadcn.mjs +10 -6
  78. package/dist/typeInference-k7FXfTVO.d.mts +335 -0
  79. package/dist/types-D_5ST7SS.d.mts +269 -0
  80. package/dist/version-B5NV-35j.d.mts +69 -0
  81. package/package.json +1 -1
  82. package/dist/types-BJzEgJdX.d.mts +0 -335
  83. /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 { b as ComponentResolver } from "../types-BJzEgJdX.mjs";
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, toReactNode };
11
+ export { headlessResolver };
@@ -1,496 +1,6 @@
1
- import { isObject } from "../core/guards.mjs";
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, toReactNode };
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 };