schema-components 1.0.0 → 1.2.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.
@@ -0,0 +1,41 @@
1
+ import { b as ComponentResolver, m as SchemaMeta } from "../types-DDCD6Xnx.mjs";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/react/SchemaView.d.ts
5
+ interface SchemaViewProps {
6
+ /** Zod schema, JSON Schema object, or OpenAPI document. */
7
+ schema: unknown;
8
+ /** For OpenAPI: a ref string like "#/components/schemas/User". */
9
+ ref?: string;
10
+ /** Current value to render. */
11
+ value?: unknown;
12
+ /** Per-field meta overrides. */
13
+ fields?: Record<string, unknown>;
14
+ /** Meta overrides applied to the root schema. */
15
+ meta?: SchemaMeta;
16
+ /** Convenience: sets description on the root. */
17
+ description?: string;
18
+ /**
19
+ * Theme resolver. In a Server Component you pass this explicitly
20
+ * since `SchemaProvider` (React context) is unavailable.
21
+ * Falls back to the headless resolver if omitted.
22
+ */
23
+ resolver?: ComponentResolver;
24
+ }
25
+ /**
26
+ * Server-safe schema renderer — no hooks, no context, no state.
27
+ *
28
+ * Always renders in read-only mode. For editable forms, use
29
+ * `<SchemaComponent>` with `"use client"`.
30
+ */
31
+ declare function SchemaView({
32
+ schema: schemaInput,
33
+ ref: refInput,
34
+ value,
35
+ fields,
36
+ meta: componentMeta,
37
+ description,
38
+ resolver
39
+ }: SchemaViewProps): ReactNode;
40
+ //#endregion
41
+ export { SchemaView, SchemaViewProps };
@@ -0,0 +1,102 @@
1
+ import { normaliseSchema } from "../core/adapter.mjs";
2
+ import { SchemaNormalisationError, SchemaRenderError } from "../core/errors.mjs";
3
+ import { getRenderFunction, mergeResolvers } from "../core/renderer.mjs";
4
+ import { walk } from "../core/walker.mjs";
5
+ import { headlessResolver } from "./headless.mjs";
6
+ import { isValidElement } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/react/SchemaView.tsx
9
+ /**
10
+ * React Server Component for read-only schema rendering.
11
+ *
12
+ * This component has zero hooks — no `useContext`, no `useMemo`,
13
+ * no `useCallback`. It can run in a React Server Component environment
14
+ * without the `"use client"` directive.
15
+ *
16
+ * **Read-only only.** For interactive forms with `onChange`, use
17
+ * `<SchemaComponent>` (which requires `"use client"`).
18
+ *
19
+ * Usage in a Server Component:
20
+ * ```tsx
21
+ * import { SchemaView } from "schema-components/react/SchemaView";
22
+ *
23
+ * export default async function Page() {
24
+ * const user = await getUser();
25
+ * return <SchemaView schema={userSchema} value={user} />;
26
+ * }
27
+ * ```
28
+ *
29
+ * The `resolver` prop replaces the `SchemaProvider` context —
30
+ * Server Components cannot use React context, so the resolver
31
+ * is passed explicitly.
32
+ */
33
+ function noop() {}
34
+ /**
35
+ * Server-safe schema renderer — no hooks, no context, no state.
36
+ *
37
+ * Always renders in read-only mode. For editable forms, use
38
+ * `<SchemaComponent>` with `"use client"`.
39
+ */
40
+ function SchemaView({ schema: schemaInput, ref: refInput, value, fields, meta: componentMeta, description, resolver }) {
41
+ const mergedMeta = {
42
+ ...componentMeta,
43
+ readOnly: true
44
+ };
45
+ if (description !== void 0) mergedMeta.description = description;
46
+ let jsonSchema;
47
+ let rootMeta;
48
+ let rootDocument;
49
+ try {
50
+ const normalised = normaliseSchema(schemaInput, refInput);
51
+ jsonSchema = normalised.jsonSchema;
52
+ rootMeta = normalised.rootMeta;
53
+ rootDocument = normalised.rootDocument;
54
+ } catch (err) {
55
+ throw new SchemaNormalisationError(err instanceof Error ? err.message : "Failed to normalise schema", schemaInput, "unknown");
56
+ }
57
+ const tree = walk(jsonSchema, {
58
+ componentMeta: mergedMeta,
59
+ rootMeta,
60
+ fieldOverrides: fields,
61
+ rootDocument
62
+ });
63
+ const userResolver = resolver !== void 0 ? mergeResolvers(resolver, headlessResolver) : headlessResolver;
64
+ const renderChild = (childTree, childValue) => renderFieldServer(childTree, childValue, userResolver, renderChild);
65
+ return renderFieldServer(tree, value ?? tree.defaultValue, userResolver, renderChild);
66
+ }
67
+ function renderFieldServer(tree, value, resolver, renderChild) {
68
+ const renderFn = getRenderFunction(tree.type, resolver);
69
+ if (renderFn !== void 0) {
70
+ const props = {
71
+ value,
72
+ onChange: noop,
73
+ readOnly: true,
74
+ writeOnly: false,
75
+ meta: tree.meta,
76
+ constraints: tree.constraints,
77
+ path: "",
78
+ tree,
79
+ renderChild: (childTree, childValue) => renderChild(childTree, childValue)
80
+ };
81
+ if (tree.enumValues !== void 0) props.enumValues = tree.enumValues;
82
+ if (tree.element !== void 0) props.element = tree.element;
83
+ if (tree.fields !== void 0) props.fields = tree.fields;
84
+ if (tree.options !== void 0) props.options = tree.options;
85
+ if (tree.discriminator !== void 0) props.discriminator = tree.discriminator;
86
+ if (tree.keyType !== void 0) props.keyType = tree.keyType;
87
+ if (tree.valueType !== void 0) props.valueType = tree.valueType;
88
+ try {
89
+ const result = renderFn(props);
90
+ if (result !== void 0 && result !== null) {
91
+ if (isValidElement(result)) return result;
92
+ if (typeof result === "string" || typeof result === "number") return result;
93
+ }
94
+ } catch (err) {
95
+ throw new SchemaRenderError(err instanceof Error ? err.message : `Render function threw for type "${tree.type}"`, tree, tree.type, err);
96
+ }
97
+ }
98
+ if (value === void 0 || value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
99
+ return /* @__PURE__ */ jsx("span", { children: typeof value === "string" ? value : JSON.stringify(value) });
100
+ }
101
+ //#endregion
102
+ export { SchemaView };
@@ -1,4 +1,4 @@
1
- import { b as ComponentResolver } from "../types-BU0ETFHk.mjs";
1
+ import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
2
2
  import { ReactNode } from "react";
3
3
 
4
4
  //#region src/react/headless.d.ts
@@ -1,5 +1,5 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
- import { isValidElement } from "react";
2
+ import { isValidElement, useCallback, useRef } from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/react/headless.tsx
5
5
  /**
@@ -8,6 +8,13 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  * Produces plain HTML elements for every schema type. Theme adapters
9
9
  * replace this by implementing ComponentResolver with their own components.
10
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
+ *
11
18
  * This module imports React and lives in the react layer, not core,
12
19
  * because it produces ReactNode values.
13
20
  */
@@ -17,36 +24,122 @@ function toReactNode(value) {
17
24
  if (isValidElement(value)) return value;
18
25
  return null;
19
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
+ }
20
70
  function renderString(props) {
71
+ const id = inputId(props.path);
21
72
  if (props.readOnly) {
22
73
  const strValue = typeof props.value === "string" ? props.value : void 0;
23
- if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", { children: "—" });
74
+ if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", {
75
+ id,
76
+ "aria-readonly": "true",
77
+ children: "\\u2014"
78
+ });
24
79
  const format = props.constraints.format;
25
80
  if (format === "email") return /* @__PURE__ */ jsx("a", {
26
81
  href: `mailto:${strValue}`,
82
+ id,
83
+ "aria-readonly": "true",
27
84
  children: strValue
28
85
  });
29
86
  if (format === "uri" || format === "url") return /* @__PURE__ */ jsx("a", {
30
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",
31
110
  children: strValue
32
111
  });
33
- return /* @__PURE__ */ jsx("span", { children: strValue });
34
112
  }
35
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
+ });
36
126
  if (props.enumValues !== void 0 && props.enumValues.length > 0) return /* @__PURE__ */ jsxs("select", {
127
+ id,
37
128
  value: strValue,
38
129
  onChange: (e) => {
39
130
  props.onChange(e.target.value);
40
131
  },
132
+ ...ariaAttrs,
41
133
  children: [/* @__PURE__ */ jsx("option", {
42
134
  value: "",
43
- children: "Select"
135
+ children: "Select\\u2026"
44
136
  }), props.enumValues.map((v) => /* @__PURE__ */ jsx("option", {
45
137
  value: v,
46
138
  children: v
47
139
  }, v))]
48
140
  });
49
141
  return /* @__PURE__ */ jsx("input", {
142
+ id,
50
143
  type: props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text",
51
144
  value: props.writeOnly ? "" : strValue,
52
145
  onChange: (e) => {
@@ -54,49 +147,86 @@ function renderString(props) {
54
147
  },
55
148
  placeholder: typeof props.meta.description === "string" ? props.meta.description : void 0,
56
149
  minLength: props.constraints.minLength,
57
- maxLength: props.constraints.maxLength
150
+ maxLength: props.constraints.maxLength,
151
+ ...ariaAttrs
58
152
  });
59
153
  }
60
154
  function renderNumber(props) {
155
+ const id = inputId(props.path);
61
156
  if (props.readOnly) {
62
- if (typeof props.value !== "number") return /* @__PURE__ */ jsx("span", { children: "—" });
63
- return /* @__PURE__ */ jsx("span", { children: props.value.toLocaleString() });
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
+ });
64
167
  }
65
168
  const numValue = typeof props.value === "number" ? props.value : "";
169
+ const ariaAttrs = {};
170
+ if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
66
171
  return /* @__PURE__ */ jsx("input", {
172
+ id,
67
173
  type: "number",
68
174
  value: props.writeOnly ? "" : numValue,
69
175
  onChange: (e) => {
70
176
  props.onChange(Number(e.target.value));
71
177
  },
72
178
  min: props.constraints.minimum,
73
- max: props.constraints.maximum
179
+ max: props.constraints.maximum,
180
+ ...ariaAttrs
74
181
  });
75
182
  }
76
183
  function renderBoolean(props) {
184
+ const id = inputId(props.path);
77
185
  if (props.readOnly) {
78
- if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx("span", { children: "—" });
79
- return /* @__PURE__ */ jsx("span", { children: props.value ? "Yes" : "No" });
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
+ });
80
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;
81
200
  return /* @__PURE__ */ jsx("input", {
201
+ id,
82
202
  type: "checkbox",
83
203
  checked: props.value === true,
84
204
  onChange: (e) => {
85
205
  props.onChange(e.target.checked);
86
- }
206
+ },
207
+ ...ariaAttrs
87
208
  });
88
209
  }
89
210
  function renderEnum(props) {
211
+ const id = inputId(props.path);
90
212
  const enumValue = typeof props.value === "string" ? props.value : "";
91
- if (props.readOnly) return /* @__PURE__ */ jsx("span", { children: enumValue || "—" });
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";
92
220
  return /* @__PURE__ */ jsxs("select", {
221
+ id,
93
222
  value: props.writeOnly ? "" : enumValue,
94
223
  onChange: (e) => {
95
224
  props.onChange(e.target.value);
96
225
  },
226
+ ...ariaAttrs,
97
227
  children: [/* @__PURE__ */ jsx("option", {
98
228
  value: "",
99
- children: "Select"
229
+ children: "Select\\u2026"
100
230
  }), props.enumValues?.map((v) => /* @__PURE__ */ jsx("option", {
101
231
  value: v,
102
232
  children: v
@@ -109,35 +239,210 @@ function renderObject(props) {
109
239
  if (fields === void 0) return null;
110
240
  return /* @__PURE__ */ jsxs("fieldset", { children: [typeof props.meta.description === "string" && /* @__PURE__ */ jsx("legend", { children: props.meta.description }), Object.entries(fields).map(([key, field]) => {
111
241
  const childValue = obj[key];
242
+ const childId = inputId(props.path ? `${props.path}.${key}` : key);
112
243
  const childOnChange = (v) => {
113
244
  const updated = {};
114
245
  for (const [k, val] of Object.entries(obj)) updated[k] = val;
115
246
  updated[key] = v;
116
247
  props.onChange(updated);
117
248
  };
118
- return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsx("label", { children: field.meta.description }), toReactNode(props.renderChild(field, childValue, childOnChange))] }, key);
249
+ return /* @__PURE__ */ jsxs("div", { children: [typeof field.meta.description === "string" && /* @__PURE__ */ jsxs("label", {
250
+ htmlFor: childId,
251
+ children: [field.meta.description, field.isOptional === false && /* @__PURE__ */ jsxs("span", {
252
+ "aria-hidden": "true",
253
+ style: { color: "#dc2626" },
254
+ children: [" ", "*"]
255
+ })]
256
+ }), toReactNode(props.renderChild(field, childValue, childOnChange))] }, key);
119
257
  })] });
120
258
  }
121
259
  function renderArray(props) {
122
260
  const arr = Array.isArray(props.value) ? props.value : [];
123
261
  const element = props.element;
124
262
  if (element === void 0) return null;
125
- return /* @__PURE__ */ jsx("div", { children: arr.map((item, i) => {
126
- const childOnChange = (v) => {
127
- const next = arr.slice();
128
- next[i] = v;
129
- props.onChange(next);
130
- };
131
- return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
132
- }) });
263
+ return /* @__PURE__ */ jsx("div", {
264
+ role: "group",
265
+ "aria-label": props.meta.description ?? void 0,
266
+ children: arr.map((item, i) => {
267
+ const childOnChange = (v) => {
268
+ const next = arr.slice();
269
+ next[i] = v;
270
+ props.onChange(next);
271
+ };
272
+ return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, item, childOnChange)) }, String(i));
273
+ })
274
+ });
275
+ }
276
+ function renderUnion(props) {
277
+ const options = props.options;
278
+ if (options === void 0 || options.length === 0) {
279
+ if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
280
+ return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
281
+ }
282
+ const matched = matchUnionOption(options, props.value);
283
+ if (matched !== void 0) return toReactNode(props.renderChild(matched, props.value, props.onChange));
284
+ const firstOption = options[0];
285
+ if (firstOption !== void 0) return toReactNode(props.renderChild(firstOption, props.value, props.onChange));
286
+ return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
287
+ }
288
+ function renderDiscriminatedUnion(props) {
289
+ const options = props.options;
290
+ const discriminator = props.discriminator;
291
+ if (options === void 0 || options.length === 0) {
292
+ if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
293
+ return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
294
+ }
295
+ const obj = isObject(props.value) ? props.value : {};
296
+ const discKey = discriminator ?? "";
297
+ const currentDiscriminatorValue = typeof obj[discKey] === "string" ? obj[discKey] : void 0;
298
+ const optionLabels = options.map((opt) => {
299
+ const discriminatorField = opt.fields?.[discKey];
300
+ if (discriminatorField !== void 0) {
301
+ const constVal = discriminatorField.literalValues?.[0];
302
+ if (typeof constVal === "string") return constVal;
303
+ }
304
+ return typeof opt.meta.title === "string" ? opt.meta.title : opt.type;
305
+ });
306
+ let activeIndex = 0;
307
+ if (currentDiscriminatorValue !== void 0) {
308
+ const found = optionLabels.indexOf(currentDiscriminatorValue);
309
+ if (found !== -1) activeIndex = found;
310
+ }
311
+ const activeOption = options[activeIndex];
312
+ const panelId = inputId(props.path);
313
+ if (props.readOnly) {
314
+ if (activeOption !== void 0) return toReactNode(props.renderChild(activeOption, props.value, props.onChange));
315
+ return /* @__PURE__ */ jsx("span", { children: "\\u2014" });
316
+ }
317
+ return /* @__PURE__ */ jsx(DiscriminatedUnionTabs, {
318
+ options,
319
+ optionLabels,
320
+ activeIndex,
321
+ panelId,
322
+ discKey,
323
+ props
324
+ });
325
+ }
326
+ /**
327
+ * WAI-ARIA tabs component for discriminated unions.
328
+ * Implements the full tabs keyboard pattern:
329
+ * - Left/Right arrow keys move between tabs
330
+ * - Home/End move to first/last tab
331
+ * - Tab moves focus into the active panel
332
+ * - aria-selected, aria-controls, role="tablist"/"tab"/"tabpanel"
333
+ */
334
+ function DiscriminatedUnionTabs({ options, optionLabels, activeIndex, panelId, discKey, props }) {
335
+ const tabRefs = useRef([]);
336
+ const handleTabChange = useCallback((newIndex) => {
337
+ const label = optionLabels[newIndex];
338
+ if (label === void 0) return;
339
+ props.onChange({ [discKey]: label });
340
+ }, [
341
+ optionLabels,
342
+ discKey,
343
+ props
344
+ ]);
345
+ const focusTab = useCallback((index) => {
346
+ const clamped = (index % options.length + options.length) % options.length;
347
+ tabRefs.current[clamped]?.focus();
348
+ }, [options.length]);
349
+ const handleKeyDown = useCallback((e) => {
350
+ if (e.key === "ArrowRight") {
351
+ e.preventDefault();
352
+ focusTab(activeIndex + 1);
353
+ } else if (e.key === "ArrowLeft") {
354
+ e.preventDefault();
355
+ focusTab(activeIndex - 1);
356
+ } else if (e.key === "Home") {
357
+ e.preventDefault();
358
+ focusTab(0);
359
+ } else if (e.key === "End") {
360
+ e.preventDefault();
361
+ focusTab(options.length - 1);
362
+ }
363
+ }, [
364
+ activeIndex,
365
+ focusTab,
366
+ options.length
367
+ ]);
368
+ const activeOption = options[activeIndex];
369
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
370
+ role: "tablist",
371
+ "aria-label": "Select variant",
372
+ style: {
373
+ display: "flex",
374
+ gap: "0.25rem",
375
+ marginBottom: "0.5rem"
376
+ },
377
+ onKeyDown: handleKeyDown,
378
+ children: options.map((_opt, i) => /* @__PURE__ */ jsx("button", {
379
+ ref: (el) => {
380
+ tabRefs.current[i] = el;
381
+ },
382
+ type: "button",
383
+ role: "tab",
384
+ "aria-selected": i === activeIndex ? "true" : void 0,
385
+ "aria-controls": `${panelId}-panel`,
386
+ tabIndex: i === activeIndex ? 0 : -1,
387
+ onClick: () => {
388
+ handleTabChange(i);
389
+ },
390
+ style: {
391
+ padding: "0.25rem 0.75rem",
392
+ border: i === activeIndex ? "1px solid #3b82f6" : "1px solid #d1d5db",
393
+ borderRadius: "0.25rem",
394
+ background: i === activeIndex ? "#eff6ff" : "transparent",
395
+ cursor: "pointer",
396
+ fontSize: "0.875rem"
397
+ },
398
+ children: optionLabels[i]
399
+ }, String(i)))
400
+ }), /* @__PURE__ */ jsx("div", {
401
+ role: "tabpanel",
402
+ id: `${panelId}-panel`,
403
+ "aria-labelledby": `${panelId}-tab-${String(activeIndex)}`,
404
+ children: activeOption !== void 0 && toReactNode(props.renderChild(activeOption, props.value, props.onChange))
405
+ })] });
406
+ }
407
+ function renderFile(props) {
408
+ const id = inputId(props.path);
409
+ const accept = props.constraints.mimeTypes?.join(",");
410
+ if (props.readOnly) return /* @__PURE__ */ jsx("span", {
411
+ id,
412
+ "aria-readonly": "true",
413
+ children: "File field"
414
+ });
415
+ const ariaAttrs = {};
416
+ if (props.tree.isOptional === false) ariaAttrs["aria-required"] = "true";
417
+ if (typeof props.meta.description === "string") ariaAttrs["aria-label"] = props.meta.description;
418
+ return /* @__PURE__ */ jsx("input", {
419
+ id,
420
+ type: "file",
421
+ accept,
422
+ onChange: (e) => {
423
+ const file = e.target.files?.[0];
424
+ if (file !== void 0) props.onChange(file);
425
+ },
426
+ ...ariaAttrs
427
+ });
133
428
  }
134
429
  function renderUnknown(props) {
430
+ const id = inputId(props.path);
135
431
  if (props.readOnly) {
136
- if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
137
- return /* @__PURE__ */ jsx("span", { children: typeof props.value === "string" ? props.value : JSON.stringify(props.value) });
432
+ if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", {
433
+ id,
434
+ "aria-readonly": "true",
435
+ children: "\\u2014"
436
+ });
437
+ return /* @__PURE__ */ jsx("span", {
438
+ id,
439
+ "aria-readonly": "true",
440
+ children: typeof props.value === "string" ? props.value : JSON.stringify(props.value)
441
+ });
138
442
  }
139
443
  const strValue = typeof props.value === "string" ? props.value : "";
140
444
  return /* @__PURE__ */ jsx("input", {
445
+ id,
141
446
  type: "text",
142
447
  value: props.writeOnly ? "" : strValue,
143
448
  onChange: (e) => {
@@ -145,6 +450,13 @@ function renderUnknown(props) {
145
450
  }
146
451
  });
147
452
  }
453
+ function matchUnionOption(options, value) {
454
+ if (typeof value === "string") return options.find((o) => o.type === "string" || o.type === "enum");
455
+ if (typeof value === "number") return options.find((o) => o.type === "number");
456
+ if (typeof value === "boolean") return options.find((o) => o.type === "boolean");
457
+ if (Array.isArray(value)) return options.find((o) => o.type === "array");
458
+ if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
459
+ }
148
460
  /**
149
461
  * The headless resolver uses props.renderChild for recursive rendering.
150
462
  * No factory function needed — the renderChild is always available
@@ -157,6 +469,9 @@ const headlessResolver = {
157
469
  enum: renderEnum,
158
470
  object: renderObject,
159
471
  array: renderArray,
472
+ union: renderUnion,
473
+ discriminatedUnion: renderDiscriminatedUnion,
474
+ file: renderFile,
160
475
  unknown: renderUnknown
161
476
  };
162
477
  //#endregion
@@ -0,0 +1,17 @@
1
+ import { b as ComponentResolver } from "../types-DDCD6Xnx.mjs";
2
+
3
+ //#region src/themes/mui.d.ts
4
+ /**
5
+ * Register real MUI components. Call once at app startup.
6
+ */
7
+ declare function registerMuiComponents(components: {
8
+ TextField: React.ComponentType<Record<string, unknown>>;
9
+ Checkbox: React.ComponentType<Record<string, unknown>>;
10
+ Typography: React.ComponentType<Record<string, unknown>>;
11
+ Box: React.ComponentType<Record<string, unknown>>;
12
+ MenuItem: React.ComponentType<Record<string, unknown>>;
13
+ FormControlLabel: React.ComponentType<Record<string, unknown>>;
14
+ }): void;
15
+ declare const muiResolver: ComponentResolver;
16
+ //#endregion
17
+ export { muiResolver, registerMuiComponents };