schema-components 2.0.2 → 2.1.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 +98 -1
- package/dist/SchemaComponent-B__6-5-E.d.mts +277 -0
- package/dist/SchemaComponent-BxzzsHsK.mjs +668 -0
- package/dist/adapter-ktQaheWB.d.mts +213 -0
- package/dist/constructorTypes-BdCiMS6e.d.mts +30 -0
- package/dist/core/adapter.d.mts +3 -213
- package/dist/core/constraintHint.d.mts +1 -1
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/contexts.d.mts +71 -0
- package/dist/core/contexts.mjs +1 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/{react → core}/fieldPath.d.mts +2 -2
- package/dist/{react → core}/fieldPath.mjs +3 -3
- package/dist/core/formats.d.mts +1 -1
- package/dist/core/inferValue.d.mts +1 -1
- package/dist/core/limits.d.mts +1 -1
- package/dist/core/merge.d.mts +1 -1
- package/dist/core/normalise.d.mts +2 -2
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/renderField.d.mts +147 -0
- package/dist/core/renderField.mjs +81 -0
- package/dist/core/renderer.d.mts +2 -199
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/typeInference.d.mts +1 -982
- package/dist/core/types.d.mts +1 -1
- package/dist/core/unionMatch.d.mts +1 -1
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +3 -3
- package/dist/core/walker.d.mts +1 -1
- package/dist/{errors-Dki7tji4.d.mts → errors-DbaI04x2.d.mts} +1 -1
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/renderToHtml.d.mts +5 -5
- package/dist/html/renderToHtml.mjs +33 -18
- package/dist/html/renderToHtmlStream.d.mts +5 -5
- package/dist/html/renderers.d.mts +1 -1
- package/dist/html/streamRenderers.d.mts +3 -3
- package/dist/{inferValue-Ce-PviSD.d.mts → inferValue-eAnh50EM.d.mts} +3 -3
- package/dist/lit/SchemaComponent.d.mts +125 -0
- package/dist/lit/SchemaComponent.mjs +2 -0
- package/dist/lit/SchemaField.d.mts +65 -0
- package/dist/lit/SchemaField.mjs +2 -0
- package/dist/lit/SchemaView.d.mts +14 -0
- package/dist/lit/SchemaView.mjs +2 -0
- package/dist/lit/constructorTypes.d.mts +2 -0
- package/dist/lit/constructorTypes.mjs +1 -0
- package/dist/lit/contexts.d.mts +78 -0
- package/dist/lit/contexts.mjs +238 -0
- package/dist/lit/defaultResolver.d.mts +33 -0
- package/dist/lit/defaultResolver.mjs +2 -0
- package/dist/lit/registry.d.mts +66 -0
- package/dist/lit/registry.mjs +2 -0
- package/dist/lit/renderers/baseElement.d.mts +131 -0
- package/dist/lit/renderers/baseElement.mjs +109 -0
- package/dist/lit/renderers/recordHelpers.d.mts +25 -0
- package/dist/lit/renderers/recordHelpers.mjs +55 -0
- package/dist/lit/renderers/scArray.d.mts +14 -0
- package/dist/lit/renderers/scArray.mjs +86 -0
- package/dist/lit/renderers/scBoolean.d.mts +15 -0
- package/dist/lit/renderers/scBoolean.mjs +47 -0
- package/dist/lit/renderers/scConditional.d.mts +23 -0
- package/dist/lit/renderers/scConditional.mjs +65 -0
- package/dist/lit/renderers/scDiscriminated.d.mts +23 -0
- package/dist/lit/renderers/scDiscriminated.mjs +138 -0
- package/dist/lit/renderers/scEnum.d.mts +16 -0
- package/dist/lit/renderers/scEnum.mjs +66 -0
- package/dist/lit/renderers/scFile.d.mts +15 -0
- package/dist/lit/renderers/scFile.mjs +53 -0
- package/dist/lit/renderers/scLiteralNullNever.d.mts +30 -0
- package/dist/lit/renderers/scLiteralNullNever.mjs +57 -0
- package/dist/lit/renderers/scNumber.d.mts +15 -0
- package/dist/lit/renderers/scNumber.mjs +64 -0
- package/dist/lit/renderers/scObject.d.mts +14 -0
- package/dist/lit/renderers/scObject.mjs +57 -0
- package/dist/lit/renderers/scRecord.d.mts +14 -0
- package/dist/lit/renderers/scRecord.mjs +112 -0
- package/dist/lit/renderers/scString.d.mts +19 -0
- package/dist/lit/renderers/scString.mjs +165 -0
- package/dist/lit/renderers/scTuple.d.mts +14 -0
- package/dist/lit/renderers/scTuple.mjs +58 -0
- package/dist/lit/renderers/scUnion.d.mts +14 -0
- package/dist/lit/renderers/scUnion.mjs +44 -0
- package/dist/lit/renderers/scUnknown.d.mts +15 -0
- package/dist/lit/renderers/scUnknown.mjs +45 -0
- package/dist/lit/ssr.d.mts +37 -0
- package/dist/lit/ssr.mjs +9565 -0
- package/dist/lit/types.d.mts +2 -0
- package/dist/lit/types.mjs +1 -0
- package/dist/lit/widget.d.mts +71 -0
- package/dist/lit/widget.mjs +87 -0
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/components.d.mts +4 -4
- package/dist/openapi/parser.d.mts +2 -2
- package/dist/openapi/resolve.d.mts +1 -1
- package/dist/preact/SchemaComponent.d.mts +3 -0
- package/dist/preact/SchemaComponent.mjs +26 -0
- package/dist/preact/SchemaErrorBoundary.d.mts +2 -0
- package/dist/preact/SchemaErrorBoundary.mjs +20 -0
- package/dist/preact/SchemaView.d.mts +2 -0
- package/dist/preact/SchemaView.mjs +22 -0
- package/dist/preact/headless.d.mts +2 -0
- package/dist/preact/headless.mjs +18 -0
- package/dist/react/SchemaComponent.d.mts +3 -270
- package/dist/react/SchemaComponent.mjs +41 -32
- package/dist/react/SchemaView.d.mts +6 -6
- package/dist/react/SchemaView.mjs +32 -29
- package/dist/react/a11y.d.mts +2 -2
- package/dist/react/fieldShell.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +2 -2
- package/dist/{ref-DdsbekXX.d.mts → ref-DWrQG1Er.d.mts} +1 -1
- package/dist/renderer-ab9E52Bp.d.mts +245 -0
- package/dist/solid/SchemaComponent.d.mts +136 -0
- package/dist/solid/SchemaComponent.mjs +391 -0
- package/dist/solid/SchemaErrorBoundary.d.mts +38 -0
- package/dist/solid/SchemaErrorBoundary.mjs +57 -0
- package/dist/solid/SchemaField.d.mts +40 -0
- package/dist/solid/SchemaField.mjs +113 -0
- package/dist/solid/SchemaView.d.mts +54 -0
- package/dist/solid/SchemaView.mjs +168 -0
- package/dist/solid/a11y.d.mts +70 -0
- package/dist/solid/a11y.mjs +71 -0
- package/dist/solid/contexts.d.mts +37 -0
- package/dist/solid/contexts.mjs +66 -0
- package/dist/solid/headless.d.mts +10 -0
- package/dist/solid/headless.mjs +27 -0
- package/dist/solid/renderers.d.mts +79 -0
- package/dist/solid/renderers.mjs +840 -0
- package/dist/solid/types.d.mts +90 -0
- package/dist/solid/types.mjs +1 -0
- package/dist/solid/widget.d.mts +29 -0
- package/dist/solid/widget.mjs +35 -0
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/typeInference-Y8tNEQJk.d.mts +983 -0
- package/dist/types-BCy7K3nk.d.mts +125 -0
- package/package.json +71 -1
- package/src/svelte/SchemaComponent.svelte +427 -0
- package/src/svelte/SchemaErrorBoundary.svelte +66 -0
- package/src/svelte/SchemaField.svelte +216 -0
- package/src/svelte/SchemaProvider.svelte +46 -0
- package/src/svelte/SchemaView.svelte +244 -0
- package/src/svelte/a11y.ts +112 -0
- package/src/svelte/contexts.ts +79 -0
- package/src/svelte/dispatch.ts +267 -0
- package/src/svelte/headless.ts +73 -0
- package/src/svelte/headlessFns.ts +124 -0
- package/src/svelte/renderers/Array.svelte +98 -0
- package/src/svelte/renderers/Boolean.svelte +43 -0
- package/src/svelte/renderers/Conditional.svelte +67 -0
- package/src/svelte/renderers/DiscriminatedUnion.svelte +197 -0
- package/src/svelte/renderers/Enum.svelte +53 -0
- package/src/svelte/renderers/Fallback.svelte +24 -0
- package/src/svelte/renderers/File.svelte +46 -0
- package/src/svelte/renderers/Literal.svelte +29 -0
- package/src/svelte/renderers/Mount.svelte +24 -0
- package/src/svelte/renderers/Negation.svelte +35 -0
- package/src/svelte/renderers/Never.svelte +24 -0
- package/src/svelte/renderers/Null.svelte +19 -0
- package/src/svelte/renderers/Number.svelte +68 -0
- package/src/svelte/renderers/Object.svelte +74 -0
- package/src/svelte/renderers/Record.svelte +134 -0
- package/src/svelte/renderers/RecursionSentinel.svelte +27 -0
- package/src/svelte/renderers/String.svelte +152 -0
- package/src/svelte/renderers/Tuple.svelte +84 -0
- package/src/svelte/renderers/Union.svelte +49 -0
- package/src/svelte/renderers/Unknown.svelte +42 -0
- package/src/svelte/svelte-modules.d.ts +25 -0
- package/src/svelte/types.ts +238 -0
- package/src/svelte/widget.ts +62 -0
- /package/dist/{diagnostics-BTrm3O6J.d.mts → diagnostics-mftUZI7c.d.mts} +0 -0
- /package/dist/{limits-x4OiyJxh.d.mts → limits-Vv9hUbI_.d.mts} +0 -0
- /package/dist/{types-BrYbjC7_.d.mts → types-BBQaEPfE.d.mts} +0 -0
- /package/dist/{version-DL8U5RuA.d.mts → version-BEBx10ND.d.mts} +0 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { isObject } from "../core/guards.mjs";
|
|
2
|
+
import { dateInputType } from "../core/formats.mjs";
|
|
3
|
+
import { isSafeHyperlink, isSafeMailtoAddress } from "../core/uri.mjs";
|
|
4
|
+
import { SC_CLASSES } from "../core/cssClasses.mjs";
|
|
5
|
+
import { sortFieldsByOrder } from "../core/fieldOrder.mjs";
|
|
6
|
+
import { fieldDomId, panelIdFor, tabIdFor } from "../core/idPath.mjs";
|
|
7
|
+
import { matchUnionOption, resolveDiscriminatedActive } from "../core/unionMatch.mjs";
|
|
8
|
+
import { displayJsonValue } from "../core/walkBuilders.mjs";
|
|
9
|
+
import { ariaLabel, buildAriaAttrs, buildHintInfo } from "./a11y.mjs";
|
|
10
|
+
import { For, Show, createEffect, createSignal } from "solid-js";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "solid-js/jsx-runtime";
|
|
12
|
+
//#region src/solid/renderers.tsx
|
|
13
|
+
/** @jsxImportSource solid-js */
|
|
14
|
+
/**
|
|
15
|
+
* Solid headless renderer functions — one per schema type.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors `react/headlessRenderers.tsx` field-for-field, adapted to
|
|
18
|
+
* Solid's idiom. Key differences:
|
|
19
|
+
*
|
|
20
|
+
* - `<Show when={...}>` replaces `condition && <X />` so the
|
|
21
|
+
* conditional branch is reactive rather than re-evaluated by parent
|
|
22
|
+
* re-renders.
|
|
23
|
+
* - `<For each={...}>` replaces `.map(...)` so list children are
|
|
24
|
+
* tracked per-item — array mutations don't tear the rendered tree
|
|
25
|
+
* down and rebuild it.
|
|
26
|
+
* - No `useRef`/`useState`/`useCallback`/`useMemo` — Solid's
|
|
27
|
+
* reactivity model makes those redundant. Element refs use the
|
|
28
|
+
* `ref={el => ...}` callback (Solid's documented escape hatch);
|
|
29
|
+
* keyboard focus state in `DiscriminatedUnionTabs` is held in a
|
|
30
|
+
* plain array with no per-render allocation.
|
|
31
|
+
* - `onChange` semantics: Solid's `onInput` fires on every keystroke
|
|
32
|
+
* (matching React's `onChange` semantics), and `onChange` on Solid
|
|
33
|
+
* fires on commit (matching the DOM `change` event). The headless
|
|
34
|
+
* renderers wire `onInput` for text/number inputs so live state
|
|
35
|
+
* updates match the React adapter's behaviour; `onChange` is used
|
|
36
|
+
* for `<input type="checkbox">`, `<input type="file">`, and
|
|
37
|
+
* `<select>` where commit semantics are the right contract.
|
|
38
|
+
*
|
|
39
|
+
* Per-type schema data is read directly from the discriminated `tree`
|
|
40
|
+
* (mirroring the React adapter); the helpers from `core/walkBuilders.ts`,
|
|
41
|
+
* `core/idPath.ts`, `core/unionMatch.ts`, and `core/cssClasses.ts` are
|
|
42
|
+
* shared between adapters.
|
|
43
|
+
*/
|
|
44
|
+
function formatDateTime(value) {
|
|
45
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
46
|
+
const date = new Date(value);
|
|
47
|
+
if (isNaN(date.getTime())) return void 0;
|
|
48
|
+
return date.toLocaleString();
|
|
49
|
+
}
|
|
50
|
+
function formatDate(value) {
|
|
51
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
52
|
+
const date = new Date(value);
|
|
53
|
+
if (isNaN(date.getTime())) return void 0;
|
|
54
|
+
return date.toLocaleDateString();
|
|
55
|
+
}
|
|
56
|
+
function formatTime(value) {
|
|
57
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
58
|
+
const date = new Date(value);
|
|
59
|
+
if (isNaN(date.getTime())) return void 0;
|
|
60
|
+
return date.toLocaleTimeString();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a stable, unique input ID from the path. Re-exported alias for
|
|
64
|
+
* {@link fieldDomId} so the Solid adapter has the same import shape as
|
|
65
|
+
* the React adapter's `inputId`.
|
|
66
|
+
*/
|
|
67
|
+
function inputId(path) {
|
|
68
|
+
return fieldDomId(path);
|
|
69
|
+
}
|
|
70
|
+
/** Headless renderer for `StringField` — plain `<input>` / `<span>`. */
|
|
71
|
+
function renderString(props) {
|
|
72
|
+
const id = inputId(props.path);
|
|
73
|
+
if (props.readOnly) {
|
|
74
|
+
const strValue = typeof props.value === "string" ? props.value : void 0;
|
|
75
|
+
if (strValue === void 0 || strValue.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
76
|
+
id,
|
|
77
|
+
children: "—"
|
|
78
|
+
});
|
|
79
|
+
const format = props.constraints.format;
|
|
80
|
+
if (format === "email" && isSafeMailtoAddress(strValue)) return /* @__PURE__ */ jsx("a", {
|
|
81
|
+
href: `mailto:${strValue}`,
|
|
82
|
+
id,
|
|
83
|
+
"aria-readonly": "true",
|
|
84
|
+
children: strValue
|
|
85
|
+
});
|
|
86
|
+
if ((format === "uri" || format === "url") && isSafeHyperlink(strValue)) 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
|
+
children: formatDate(strValue) ?? strValue
|
|
95
|
+
});
|
|
96
|
+
if (format === "time") return /* @__PURE__ */ jsx("span", {
|
|
97
|
+
id,
|
|
98
|
+
children: formatTime(strValue) ?? strValue
|
|
99
|
+
});
|
|
100
|
+
if (format === "date-time" || format === "datetime") return /* @__PURE__ */ jsx("span", {
|
|
101
|
+
id,
|
|
102
|
+
children: formatDateTime(strValue) ?? strValue
|
|
103
|
+
});
|
|
104
|
+
return /* @__PURE__ */ jsx("span", {
|
|
105
|
+
id,
|
|
106
|
+
children: strValue
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
110
|
+
const dateType = dateInputType(props.constraints.format);
|
|
111
|
+
const ariaAttrs = buildAriaAttrs(props.tree);
|
|
112
|
+
const hintInfo = buildHintInfo(id, props.constraints);
|
|
113
|
+
const hintElement = () => hintInfo === void 0 ? null : /* @__PURE__ */ jsx("small", {
|
|
114
|
+
id: hintInfo.id,
|
|
115
|
+
class: "sc-hint",
|
|
116
|
+
children: hintInfo.hint
|
|
117
|
+
});
|
|
118
|
+
if (dateType !== void 0) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
119
|
+
id,
|
|
120
|
+
type: dateType,
|
|
121
|
+
value: props.writeOnly ? "" : strValue,
|
|
122
|
+
onInput: (e) => {
|
|
123
|
+
props.onChange(e.currentTarget.value);
|
|
124
|
+
},
|
|
125
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
126
|
+
...ariaAttrs
|
|
127
|
+
}), hintElement()] });
|
|
128
|
+
if (props.tree.type === "enum" && props.tree.enumValues.length > 0) {
|
|
129
|
+
const enumValues = props.tree.enumValues;
|
|
130
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("select", {
|
|
131
|
+
id,
|
|
132
|
+
value: strValue,
|
|
133
|
+
onChange: (e) => {
|
|
134
|
+
props.onChange(e.currentTarget.value);
|
|
135
|
+
},
|
|
136
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
137
|
+
...ariaAttrs,
|
|
138
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
139
|
+
value: "",
|
|
140
|
+
children: ["Select", "…"]
|
|
141
|
+
}), /* @__PURE__ */ jsx(For, {
|
|
142
|
+
each: enumValues,
|
|
143
|
+
children: (v) => {
|
|
144
|
+
const display = displayJsonValue(v);
|
|
145
|
+
return /* @__PURE__ */ jsx("option", {
|
|
146
|
+
value: display,
|
|
147
|
+
children: display
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
})]
|
|
151
|
+
}), hintElement()] });
|
|
152
|
+
}
|
|
153
|
+
const isCredential = props.writeOnly && props.constraints.format === "password";
|
|
154
|
+
const inputType = isCredential ? "password" : props.constraints.format === "email" ? "email" : props.constraints.format === "uri" ? "url" : "text";
|
|
155
|
+
const autoComplete = isCredential ? strValue.length > 0 ? "current-password" : "new-password" : void 0;
|
|
156
|
+
const placeholder = typeof props.meta.description === "string" ? props.meta.description : void 0;
|
|
157
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
158
|
+
id,
|
|
159
|
+
type: inputType,
|
|
160
|
+
autocomplete: autoComplete,
|
|
161
|
+
value: props.writeOnly ? "" : strValue,
|
|
162
|
+
onInput: (e) => {
|
|
163
|
+
props.onChange(e.currentTarget.value);
|
|
164
|
+
},
|
|
165
|
+
placeholder,
|
|
166
|
+
minlength: props.constraints.minLength,
|
|
167
|
+
maxlength: props.constraints.maxLength,
|
|
168
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
169
|
+
...ariaAttrs
|
|
170
|
+
}), hintElement()] });
|
|
171
|
+
}
|
|
172
|
+
/** Headless renderer for `NumberField` — plain `<input type="number">`. */
|
|
173
|
+
function renderNumber(props) {
|
|
174
|
+
const id = inputId(props.path);
|
|
175
|
+
if (props.readOnly) {
|
|
176
|
+
if (typeof props.value !== "number") return /* @__PURE__ */ jsx("span", {
|
|
177
|
+
id,
|
|
178
|
+
children: "—"
|
|
179
|
+
});
|
|
180
|
+
return /* @__PURE__ */ jsx("span", {
|
|
181
|
+
id,
|
|
182
|
+
children: props.value.toLocaleString()
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const numValue = typeof props.value === "number" ? props.value : "";
|
|
186
|
+
const ariaAttrs = buildAriaAttrs(props.tree);
|
|
187
|
+
const hintInfo = buildHintInfo(id, props.constraints);
|
|
188
|
+
const isInteger = props.tree.type === "number" ? props.tree.isInteger : false;
|
|
189
|
+
const inputMode = isInteger ? "numeric" : "decimal";
|
|
190
|
+
const multipleOf = props.constraints.multipleOf;
|
|
191
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
192
|
+
id,
|
|
193
|
+
type: "number",
|
|
194
|
+
inputmode: inputMode,
|
|
195
|
+
step: multipleOf !== void 0 ? String(multipleOf) : isInteger ? "1" : void 0,
|
|
196
|
+
value: props.writeOnly ? "" : numValue,
|
|
197
|
+
onInput: (e) => {
|
|
198
|
+
props.onChange(Number(e.currentTarget.value));
|
|
199
|
+
},
|
|
200
|
+
min: props.constraints.minimum,
|
|
201
|
+
max: props.constraints.maximum,
|
|
202
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
203
|
+
...ariaAttrs
|
|
204
|
+
}), /* @__PURE__ */ jsx(Show, {
|
|
205
|
+
when: hintInfo !== void 0,
|
|
206
|
+
children: /* @__PURE__ */ jsx("small", {
|
|
207
|
+
id: hintInfo?.id,
|
|
208
|
+
class: "sc-hint",
|
|
209
|
+
children: hintInfo?.hint
|
|
210
|
+
})
|
|
211
|
+
})] });
|
|
212
|
+
}
|
|
213
|
+
/** Headless renderer for `BooleanField` — plain `<input type="checkbox">`. */
|
|
214
|
+
function renderBoolean(props) {
|
|
215
|
+
const id = inputId(props.path);
|
|
216
|
+
if (props.readOnly) {
|
|
217
|
+
if (typeof props.value !== "boolean") return /* @__PURE__ */ jsx("span", {
|
|
218
|
+
id,
|
|
219
|
+
children: "—"
|
|
220
|
+
});
|
|
221
|
+
return /* @__PURE__ */ jsx("span", {
|
|
222
|
+
id,
|
|
223
|
+
children: props.value ? "Yes" : "No"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const ariaAttrs = buildAriaAttrs(props.tree, props.meta.description);
|
|
227
|
+
return /* @__PURE__ */ jsx("input", {
|
|
228
|
+
id,
|
|
229
|
+
type: "checkbox",
|
|
230
|
+
checked: props.writeOnly ? false : props.value === true,
|
|
231
|
+
onChange: (e) => {
|
|
232
|
+
props.onChange(e.currentTarget.checked);
|
|
233
|
+
},
|
|
234
|
+
...ariaAttrs
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/** Headless renderer for `EnumField` — plain `<select>` listing each option. */
|
|
238
|
+
function renderEnum(props) {
|
|
239
|
+
const id = inputId(props.path);
|
|
240
|
+
const enumValue = typeof props.value === "string" ? props.value : "";
|
|
241
|
+
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
242
|
+
id,
|
|
243
|
+
children: enumValue.length === 0 ? "—" : enumValue
|
|
244
|
+
});
|
|
245
|
+
const ariaAttrs = buildAriaAttrs(props.tree);
|
|
246
|
+
const hintInfo = buildHintInfo(id, props.constraints);
|
|
247
|
+
const enumValues = props.tree.type === "enum" ? props.tree.enumValues : [];
|
|
248
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("select", {
|
|
249
|
+
id,
|
|
250
|
+
value: props.writeOnly ? "" : enumValue,
|
|
251
|
+
onChange: (e) => {
|
|
252
|
+
props.onChange(e.currentTarget.value);
|
|
253
|
+
},
|
|
254
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
255
|
+
...ariaAttrs,
|
|
256
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
257
|
+
value: "",
|
|
258
|
+
children: ["Select", "…"]
|
|
259
|
+
}), /* @__PURE__ */ jsx(For, {
|
|
260
|
+
each: enumValues,
|
|
261
|
+
children: (v) => {
|
|
262
|
+
const display = displayJsonValue(v);
|
|
263
|
+
return /* @__PURE__ */ jsx("option", {
|
|
264
|
+
value: display,
|
|
265
|
+
children: display
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
})]
|
|
269
|
+
}), /* @__PURE__ */ jsx(Show, {
|
|
270
|
+
when: hintInfo !== void 0,
|
|
271
|
+
children: /* @__PURE__ */ jsx("small", {
|
|
272
|
+
id: hintInfo?.id,
|
|
273
|
+
class: "sc-hint",
|
|
274
|
+
children: hintInfo?.hint
|
|
275
|
+
})
|
|
276
|
+
})] });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Compute the default value for a freshly added record entry based on
|
|
280
|
+
* the record's value-type schema. Mirrors the React headless
|
|
281
|
+
* implementation byte-for-byte so both adapters seed new entries the
|
|
282
|
+
* same way.
|
|
283
|
+
*/
|
|
284
|
+
function defaultRecordValue(valueType) {
|
|
285
|
+
if (valueType.defaultValue !== void 0) return valueType.defaultValue;
|
|
286
|
+
switch (valueType.type) {
|
|
287
|
+
case "string": return "";
|
|
288
|
+
case "number": return 0;
|
|
289
|
+
case "boolean": return false;
|
|
290
|
+
case "array": return [];
|
|
291
|
+
case "object":
|
|
292
|
+
case "record": return {};
|
|
293
|
+
case "null": return null;
|
|
294
|
+
case "unknown":
|
|
295
|
+
case "enum":
|
|
296
|
+
case "literal":
|
|
297
|
+
case "tuple":
|
|
298
|
+
case "union":
|
|
299
|
+
case "discriminatedUnion":
|
|
300
|
+
case "conditional":
|
|
301
|
+
case "negation":
|
|
302
|
+
case "file":
|
|
303
|
+
case "never": return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Generate a unique, currently-unused key for a new record entry.
|
|
308
|
+
* Picks the first of `key`, `key-1`, `key-2`, … not in `existing`.
|
|
309
|
+
*/
|
|
310
|
+
function nextRecordKey(existing, base = "key") {
|
|
311
|
+
if (!existing.includes(base)) return base;
|
|
312
|
+
let i = 1;
|
|
313
|
+
while (existing.includes(`${base}-${String(i)}`)) i += 1;
|
|
314
|
+
return `${base}-${String(i)}`;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Rename a key in an object while preserving insertion order. Returns
|
|
318
|
+
* the original object reference when the rename is a no-op
|
|
319
|
+
* (oldKey === newKey) or when newKey collides with an existing key.
|
|
320
|
+
*/
|
|
321
|
+
function renameRecordKey(obj, oldKey, newKey) {
|
|
322
|
+
if (oldKey === newKey) return obj;
|
|
323
|
+
if (newKey in obj && newKey !== oldKey) return obj;
|
|
324
|
+
const renamed = {};
|
|
325
|
+
for (const [k, v] of Object.entries(obj)) renamed[k === oldKey ? newKey : k] = v;
|
|
326
|
+
return renamed;
|
|
327
|
+
}
|
|
328
|
+
/** Headless renderer for `ObjectField` — `<fieldset>` per object. */
|
|
329
|
+
function renderObject(props) {
|
|
330
|
+
if (props.tree.type !== "object") return null;
|
|
331
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
332
|
+
const fields = props.tree.fields;
|
|
333
|
+
const sortedEntries = sortFieldsByOrder(fields).filter(([, field]) => field.meta.visible !== false);
|
|
334
|
+
return /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx(Show, {
|
|
335
|
+
when: typeof props.meta.description === "string",
|
|
336
|
+
children: /* @__PURE__ */ jsx("legend", { children: props.meta.description })
|
|
337
|
+
}), /* @__PURE__ */ jsx(For, {
|
|
338
|
+
each: sortedEntries,
|
|
339
|
+
children: ([key, field]) => {
|
|
340
|
+
const childValue = obj[key];
|
|
341
|
+
const childId = inputId(`${props.path}.${key}`);
|
|
342
|
+
const childOnChange = (v) => {
|
|
343
|
+
const updated = {};
|
|
344
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
345
|
+
updated[key] = v;
|
|
346
|
+
props.onChange(updated);
|
|
347
|
+
};
|
|
348
|
+
const child = props.renderChild(field, childValue, childOnChange, key);
|
|
349
|
+
if (child === null || child === void 0) return null;
|
|
350
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
|
|
351
|
+
for: childId,
|
|
352
|
+
children: [typeof field.meta.description === "string" ? field.meta.description : key, /* @__PURE__ */ jsx(Show, {
|
|
353
|
+
when: field.isOptional === false,
|
|
354
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
355
|
+
"aria-hidden": "true",
|
|
356
|
+
style: { color: "#dc2626" },
|
|
357
|
+
children: [" ", "*"]
|
|
358
|
+
})
|
|
359
|
+
})]
|
|
360
|
+
}), child] });
|
|
361
|
+
}
|
|
362
|
+
})] });
|
|
363
|
+
}
|
|
364
|
+
/** Headless renderer for `RecordField` — editable key/value rows. */
|
|
365
|
+
function renderRecord(props) {
|
|
366
|
+
if (props.tree.type !== "record") return null;
|
|
367
|
+
const obj = isObject(props.value) ? props.value : {};
|
|
368
|
+
const valueType = props.tree.valueType;
|
|
369
|
+
const entries = Object.entries(obj);
|
|
370
|
+
if (props.readOnly) {
|
|
371
|
+
if (entries.length === 0) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
372
|
+
return /* @__PURE__ */ jsx("div", {
|
|
373
|
+
role: "group",
|
|
374
|
+
"aria-label": ariaLabel(props.meta.description),
|
|
375
|
+
children: /* @__PURE__ */ jsx(For, {
|
|
376
|
+
each: entries,
|
|
377
|
+
children: ([key, value]) => {
|
|
378
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
379
|
+
for: inputId(`${props.path}.${key}`),
|
|
380
|
+
children: key
|
|
381
|
+
}), props.renderChild(valueType, value, () => {}, key)] });
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const handleRename = (oldKey, newKey) => {
|
|
387
|
+
const renamed = renameRecordKey(obj, oldKey, newKey);
|
|
388
|
+
if (renamed === obj) return;
|
|
389
|
+
props.onChange(renamed);
|
|
390
|
+
};
|
|
391
|
+
const handleValueChange = (key, nextValue) => {
|
|
392
|
+
const updated = {};
|
|
393
|
+
for (const [k, val] of Object.entries(obj)) updated[k] = val;
|
|
394
|
+
updated[key] = nextValue;
|
|
395
|
+
props.onChange(updated);
|
|
396
|
+
};
|
|
397
|
+
const handleRemove = (key) => {
|
|
398
|
+
const next = {};
|
|
399
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
400
|
+
if (k === key) continue;
|
|
401
|
+
next[k] = v;
|
|
402
|
+
}
|
|
403
|
+
props.onChange(next);
|
|
404
|
+
};
|
|
405
|
+
const handleAdd = () => {
|
|
406
|
+
const newKey = nextRecordKey(Object.keys(obj));
|
|
407
|
+
const next = { ...obj };
|
|
408
|
+
next[newKey] = defaultRecordValue(valueType);
|
|
409
|
+
props.onChange(next);
|
|
410
|
+
};
|
|
411
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
412
|
+
role: "group",
|
|
413
|
+
"aria-label": ariaLabel(props.meta.description),
|
|
414
|
+
children: [/* @__PURE__ */ jsx(For, {
|
|
415
|
+
each: entries,
|
|
416
|
+
children: ([key, value]) => {
|
|
417
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
418
|
+
/* @__PURE__ */ jsx("input", {
|
|
419
|
+
id: `${inputId(`${props.path}.${key}`)}-key`,
|
|
420
|
+
type: "text",
|
|
421
|
+
"aria-label": "Entry key",
|
|
422
|
+
value: key,
|
|
423
|
+
onChange: (e) => {
|
|
424
|
+
handleRename(key, e.currentTarget.value);
|
|
425
|
+
}
|
|
426
|
+
}),
|
|
427
|
+
props.renderChild(valueType, value, (nextValue) => {
|
|
428
|
+
handleValueChange(key, nextValue);
|
|
429
|
+
}, key),
|
|
430
|
+
/* @__PURE__ */ jsx("button", {
|
|
431
|
+
type: "button",
|
|
432
|
+
"aria-label": `Remove entry ${key}`,
|
|
433
|
+
onClick: () => {
|
|
434
|
+
handleRemove(key);
|
|
435
|
+
},
|
|
436
|
+
children: "Remove"
|
|
437
|
+
})
|
|
438
|
+
] });
|
|
439
|
+
}
|
|
440
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
441
|
+
type: "button",
|
|
442
|
+
"aria-label": "Add entry",
|
|
443
|
+
onClick: handleAdd,
|
|
444
|
+
children: "Add"
|
|
445
|
+
})]
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** Headless renderer for `ArrayField` — ordered list with add/remove controls. */
|
|
449
|
+
function renderArray(props) {
|
|
450
|
+
if (props.tree.type !== "array") return null;
|
|
451
|
+
const arr = Array.isArray(props.value) ? props.value : [];
|
|
452
|
+
const element = props.tree.element;
|
|
453
|
+
if (element === void 0) return null;
|
|
454
|
+
if (props.readOnly) {
|
|
455
|
+
if (arr.length === 0) return null;
|
|
456
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
457
|
+
role: "group",
|
|
458
|
+
"aria-label": ariaLabel(props.meta.description),
|
|
459
|
+
children: /* @__PURE__ */ jsx(For, {
|
|
460
|
+
each: arr,
|
|
461
|
+
children: (item, i) => /* @__PURE__ */ jsx("li", { children: props.renderChild(element, item, () => {}, `[${String(i())}]`) })
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const handleRemove = (index) => {
|
|
466
|
+
const next = arr.slice();
|
|
467
|
+
next.splice(index, 1);
|
|
468
|
+
props.onChange(next);
|
|
469
|
+
};
|
|
470
|
+
const handleAdd = () => {
|
|
471
|
+
const next = arr.slice();
|
|
472
|
+
next.push(defaultRecordValue(element));
|
|
473
|
+
props.onChange(next);
|
|
474
|
+
};
|
|
475
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
476
|
+
role: "group",
|
|
477
|
+
"aria-label": ariaLabel(props.meta.description),
|
|
478
|
+
children: [/* @__PURE__ */ jsx("ul", { children: /* @__PURE__ */ jsx(For, {
|
|
479
|
+
each: arr,
|
|
480
|
+
children: (item, i) => {
|
|
481
|
+
const childOnChange = (v) => {
|
|
482
|
+
const nextArr = arr.slice();
|
|
483
|
+
nextArr[i()] = v;
|
|
484
|
+
props.onChange(nextArr);
|
|
485
|
+
};
|
|
486
|
+
return /* @__PURE__ */ jsxs("li", { children: [props.renderChild(element, item, childOnChange, `[${String(i())}]`), /* @__PURE__ */ jsx("button", {
|
|
487
|
+
type: "button",
|
|
488
|
+
"aria-label": `Remove item ${String(i())}`,
|
|
489
|
+
onClick: () => {
|
|
490
|
+
handleRemove(i());
|
|
491
|
+
},
|
|
492
|
+
children: "Remove"
|
|
493
|
+
})] });
|
|
494
|
+
}
|
|
495
|
+
}) }), /* @__PURE__ */ jsx("button", {
|
|
496
|
+
type: "button",
|
|
497
|
+
"aria-label": "Add item",
|
|
498
|
+
onClick: handleAdd,
|
|
499
|
+
children: "Add"
|
|
500
|
+
})]
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** Headless renderer for plain `UnionField` — picks the matching option. */
|
|
504
|
+
function renderUnion(props) {
|
|
505
|
+
const options = props.tree.type === "union" || props.tree.type === "discriminatedUnion" ? props.tree.options : void 0;
|
|
506
|
+
if (options === void 0 || options.length === 0) {
|
|
507
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
508
|
+
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
509
|
+
}
|
|
510
|
+
const matched = matchUnionOption(options, props.value);
|
|
511
|
+
if (matched !== void 0) return props.renderChild(matched, props.value, props.onChange);
|
|
512
|
+
const firstOption = options[0];
|
|
513
|
+
if (firstOption !== void 0) return props.renderChild(firstOption, props.value, props.onChange);
|
|
514
|
+
return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Pure helper: convert a tab index into the new value the discriminated
|
|
518
|
+
* union should emit. Returns `undefined` when the index is out of bounds.
|
|
519
|
+
*
|
|
520
|
+
* Extracted so the contract is unit-testable without rendering the tabs
|
|
521
|
+
* component. Mirrors the React adapter's `discriminatedUnionValueForTab`.
|
|
522
|
+
*/
|
|
523
|
+
function discriminatedUnionValueForTab(optionLabels, discKey, newIndex) {
|
|
524
|
+
const label = optionLabels[newIndex];
|
|
525
|
+
if (label === void 0) return void 0;
|
|
526
|
+
return { [discKey]: label };
|
|
527
|
+
}
|
|
528
|
+
/** Headless renderer for `DiscriminatedUnionField` — tabbed UI. */
|
|
529
|
+
function renderDiscriminatedUnion(props) {
|
|
530
|
+
if (props.tree.type !== "discriminatedUnion") {
|
|
531
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
532
|
+
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
533
|
+
}
|
|
534
|
+
const { options, discriminator: discKey } = props.tree;
|
|
535
|
+
if (options.length === 0) {
|
|
536
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
537
|
+
return /* @__PURE__ */ jsx("span", { children: JSON.stringify(props.value) });
|
|
538
|
+
}
|
|
539
|
+
const { optionLabels, activeIndex, activeOption } = resolveDiscriminatedActive(options, discKey, isObject(props.value) ? props.value : void 0);
|
|
540
|
+
if (props.readOnly) {
|
|
541
|
+
if (activeOption !== void 0) return props.renderChild(activeOption, props.value, props.onChange);
|
|
542
|
+
return /* @__PURE__ */ jsx("span", { children: "—" });
|
|
543
|
+
}
|
|
544
|
+
return /* @__PURE__ */ jsx(DiscriminatedUnionTabs, {
|
|
545
|
+
options,
|
|
546
|
+
optionLabels,
|
|
547
|
+
activeIndex,
|
|
548
|
+
path: props.path,
|
|
549
|
+
discKey,
|
|
550
|
+
props
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* WAI-ARIA tabs component for discriminated unions, Solid-flavoured.
|
|
555
|
+
*
|
|
556
|
+
* Implements the WAI-ARIA "Tabs with Automatic Activation" pattern:
|
|
557
|
+
* - ArrowRight / ArrowLeft move between tabs, wrapping at the extremes.
|
|
558
|
+
* - Home / End jump to the first / last tab.
|
|
559
|
+
* - `aria-selected`, `aria-controls`, `role="tablist" | "tab" | "tabpanel"`.
|
|
560
|
+
* - Roving tabindex: the active tab carries `tabindex=0`, the rest
|
|
561
|
+
* `tabindex=-1`.
|
|
562
|
+
*
|
|
563
|
+
* "Automatic activation" means each arrow key both moves focus and
|
|
564
|
+
* activates the new tab in one step — selection and focus stay aligned.
|
|
565
|
+
*
|
|
566
|
+
* The Solid implementation differs from the React equivalent in:
|
|
567
|
+
*
|
|
568
|
+
* - Element refs use Solid's `ref={el => ...}` callback rather than
|
|
569
|
+
* `useRef`. The refs array is module-local to the JSX closure — no
|
|
570
|
+
* `createSignal` needed because we never derive reactive state from
|
|
571
|
+
* the refs themselves, only the array's contents.
|
|
572
|
+
* - A `pendingFocus` signal tracks "the user just pressed an arrow key,
|
|
573
|
+
* focus should follow the next active index". `createEffect`
|
|
574
|
+
* consumes the signal — Solid runs the effect after the DOM update
|
|
575
|
+
* so `.focus()` lands on the freshly-active button.
|
|
576
|
+
* - The handler reads `props.activeIndex` reactively because the parent
|
|
577
|
+
* re-derives it from `props.props.value` on every tab change.
|
|
578
|
+
*/
|
|
579
|
+
function DiscriminatedUnionTabs(props) {
|
|
580
|
+
const panelId = panelIdFor(props.path);
|
|
581
|
+
const tabRefs = [];
|
|
582
|
+
const [pendingFocus, setPendingFocus] = createSignal(false);
|
|
583
|
+
const handleTabChange = (newIndex) => {
|
|
584
|
+
const next = discriminatedUnionValueForTab(props.optionLabels, props.discKey, newIndex);
|
|
585
|
+
if (next === void 0) return;
|
|
586
|
+
props.props.onChange(next);
|
|
587
|
+
};
|
|
588
|
+
const wrapIndex = (index) => (index % props.options.length + props.options.length) % props.options.length;
|
|
589
|
+
const handleKeyDown = (e) => {
|
|
590
|
+
let target;
|
|
591
|
+
if (e.key === "ArrowRight") target = wrapIndex(props.activeIndex + 1);
|
|
592
|
+
else if (e.key === "ArrowLeft") target = wrapIndex(props.activeIndex - 1);
|
|
593
|
+
else if (e.key === "Home") target = 0;
|
|
594
|
+
else if (e.key === "End") target = props.options.length - 1;
|
|
595
|
+
if (target === void 0) return;
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
if (target === props.activeIndex) return;
|
|
598
|
+
setPendingFocus(true);
|
|
599
|
+
handleTabChange(target);
|
|
600
|
+
};
|
|
601
|
+
createEffect(() => {
|
|
602
|
+
if (!pendingFocus()) return;
|
|
603
|
+
const idx = props.activeIndex;
|
|
604
|
+
setPendingFocus(false);
|
|
605
|
+
tabRefs[idx]?.focus();
|
|
606
|
+
});
|
|
607
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
608
|
+
role: "tablist",
|
|
609
|
+
"aria-label": "Select variant",
|
|
610
|
+
"aria-orientation": "horizontal",
|
|
611
|
+
style: {
|
|
612
|
+
display: "flex",
|
|
613
|
+
gap: "0.25rem",
|
|
614
|
+
"margin-bottom": "0.5rem"
|
|
615
|
+
},
|
|
616
|
+
onKeyDown: handleKeyDown,
|
|
617
|
+
children: /* @__PURE__ */ jsx(For, {
|
|
618
|
+
each: props.options,
|
|
619
|
+
children: (_opt, i) => {
|
|
620
|
+
const idx = i();
|
|
621
|
+
const isActive = () => idx === props.activeIndex;
|
|
622
|
+
const tabStyle = () => ({
|
|
623
|
+
padding: "0.25rem 0.75rem",
|
|
624
|
+
border: isActive() ? "1px solid #3b82f6" : "1px solid #d1d5db",
|
|
625
|
+
"border-radius": "0.25rem",
|
|
626
|
+
background: isActive() ? "#eff6ff" : "transparent",
|
|
627
|
+
cursor: "pointer",
|
|
628
|
+
"font-size": "0.875rem"
|
|
629
|
+
});
|
|
630
|
+
return /* @__PURE__ */ jsx("button", {
|
|
631
|
+
ref: (el) => {
|
|
632
|
+
tabRefs[idx] = el;
|
|
633
|
+
},
|
|
634
|
+
type: "button",
|
|
635
|
+
role: "tab",
|
|
636
|
+
id: tabIdFor(props.path, idx),
|
|
637
|
+
"aria-selected": isActive() ? "true" : "false",
|
|
638
|
+
"aria-controls": panelId,
|
|
639
|
+
tabindex: isActive() ? 0 : -1,
|
|
640
|
+
onClick: () => {
|
|
641
|
+
handleTabChange(idx);
|
|
642
|
+
},
|
|
643
|
+
style: tabStyle(),
|
|
644
|
+
children: props.optionLabels[idx]
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
649
|
+
role: "tabpanel",
|
|
650
|
+
id: panelId,
|
|
651
|
+
"aria-labelledby": tabIdFor(props.path, props.activeIndex),
|
|
652
|
+
children: /* @__PURE__ */ jsx(Show, {
|
|
653
|
+
when: props.options[props.activeIndex],
|
|
654
|
+
children: (activeOption) => props.props.renderChild(activeOption(), props.props.value, props.props.onChange)
|
|
655
|
+
})
|
|
656
|
+
})] });
|
|
657
|
+
}
|
|
658
|
+
/** Headless renderer for `FileField` — plain `<input type="file">`. */
|
|
659
|
+
function renderFile(props) {
|
|
660
|
+
const id = inputId(props.path);
|
|
661
|
+
const accept = props.constraints.mimeTypes?.join(",");
|
|
662
|
+
if (props.readOnly) return /* @__PURE__ */ jsx("span", {
|
|
663
|
+
id,
|
|
664
|
+
children: "File field"
|
|
665
|
+
});
|
|
666
|
+
const ariaAttrs = buildAriaAttrs(props.tree, props.meta.description);
|
|
667
|
+
const hintInfo = buildHintInfo(id, props.constraints);
|
|
668
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
669
|
+
id,
|
|
670
|
+
type: "file",
|
|
671
|
+
accept,
|
|
672
|
+
onChange: (e) => {
|
|
673
|
+
const file = e.currentTarget.files?.[0];
|
|
674
|
+
if (file !== void 0) props.onChange(file);
|
|
675
|
+
},
|
|
676
|
+
"aria-describedby": hintInfo?.ariaDescribedBy,
|
|
677
|
+
...ariaAttrs
|
|
678
|
+
}), /* @__PURE__ */ jsx(Show, {
|
|
679
|
+
when: hintInfo !== void 0,
|
|
680
|
+
children: /* @__PURE__ */ jsx("small", {
|
|
681
|
+
id: hintInfo?.id,
|
|
682
|
+
class: "sc-hint",
|
|
683
|
+
children: hintInfo?.hint
|
|
684
|
+
})
|
|
685
|
+
})] });
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Render a literal field — `z.literal("a")` or `{ const: 5 }`.
|
|
689
|
+
*
|
|
690
|
+
* Literals are non-editable by nature; both read-only and editable modes
|
|
691
|
+
* display the literal value(s). Multiple literals (`z.literal(["a","b"])`)
|
|
692
|
+
* render comma-separated.
|
|
693
|
+
*/
|
|
694
|
+
function renderLiteral(props) {
|
|
695
|
+
const id = inputId(props.path);
|
|
696
|
+
if (props.tree.type !== "literal") return null;
|
|
697
|
+
const values = props.tree.literalValues;
|
|
698
|
+
if (values.length === 0) return /* @__PURE__ */ jsx("span", {
|
|
699
|
+
id,
|
|
700
|
+
children: "—"
|
|
701
|
+
});
|
|
702
|
+
return /* @__PURE__ */ jsx("span", {
|
|
703
|
+
id,
|
|
704
|
+
children: values.map((v) => displayJsonValue(v)).join(", ")
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
/** Render a null field — `z.null()` or `{ type: "null" }`. */
|
|
708
|
+
function renderNull(props) {
|
|
709
|
+
return /* @__PURE__ */ jsx("span", {
|
|
710
|
+
id: inputId(props.path),
|
|
711
|
+
children: "—"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** Render a never field — `z.never()` or `{ not: {} }` / `false` schema. */
|
|
715
|
+
function renderNever(props) {
|
|
716
|
+
return /* @__PURE__ */ jsx("span", {
|
|
717
|
+
id: inputId(props.path),
|
|
718
|
+
class: SC_CLASSES.never,
|
|
719
|
+
children: /* @__PURE__ */ jsx("em", { children: "never matches" })
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/** Render a tuple field — positional rendering of each prefix item. */
|
|
723
|
+
function renderTuple(props) {
|
|
724
|
+
if (props.tree.type !== "tuple") return null;
|
|
725
|
+
const prefixItems = props.tree.prefixItems;
|
|
726
|
+
const restItems = props.tree.restItems;
|
|
727
|
+
const arr = Array.isArray(props.value) ? props.value : [];
|
|
728
|
+
if (prefixItems.length === 0 && restItems === void 0 && arr.length === 0) return null;
|
|
729
|
+
const restCount = restItems !== void 0 ? Math.max(arr.length - prefixItems.length, 0) : 0;
|
|
730
|
+
const restRange = Array.from({ length: restCount }, (_, j) => j);
|
|
731
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
732
|
+
role: "group",
|
|
733
|
+
"aria-label": ariaLabel(props.meta.description),
|
|
734
|
+
children: [/* @__PURE__ */ jsx(For, {
|
|
735
|
+
each: prefixItems,
|
|
736
|
+
children: (element, i) => {
|
|
737
|
+
const idx = i();
|
|
738
|
+
const itemValue = arr[idx];
|
|
739
|
+
const childOnChange = (v) => {
|
|
740
|
+
const next = arr.slice();
|
|
741
|
+
next[idx] = v;
|
|
742
|
+
props.onChange(next);
|
|
743
|
+
};
|
|
744
|
+
return /* @__PURE__ */ jsx("div", { children: props.renderChild(element, itemValue, childOnChange, `[${String(idx)}]`) });
|
|
745
|
+
}
|
|
746
|
+
}), /* @__PURE__ */ jsx(Show, {
|
|
747
|
+
when: restItems,
|
|
748
|
+
children: (rest) => /* @__PURE__ */ jsx(For, {
|
|
749
|
+
each: restRange,
|
|
750
|
+
children: (j) => {
|
|
751
|
+
const idx = prefixItems.length + j;
|
|
752
|
+
const itemValue = arr[idx];
|
|
753
|
+
const childOnChange = (v) => {
|
|
754
|
+
const next = arr.slice();
|
|
755
|
+
next[idx] = v;
|
|
756
|
+
props.onChange(next);
|
|
757
|
+
};
|
|
758
|
+
return /* @__PURE__ */ jsx("div", { children: props.renderChild(rest(), itemValue, childOnChange, `[${String(idx)}]`) });
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
})]
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/** Render a conditional field — JSON Schema `if`/`then`/`else`. */
|
|
765
|
+
function renderConditional(props) {
|
|
766
|
+
if (props.tree.type !== "conditional") return null;
|
|
767
|
+
const { ifClause, thenClause, elseClause } = props.tree;
|
|
768
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
769
|
+
class: SC_CLASSES.conditional,
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ jsxs("div", {
|
|
772
|
+
class: SC_CLASSES.conditionalIf,
|
|
773
|
+
children: [
|
|
774
|
+
/* @__PURE__ */ jsx("strong", { children: "if:" }),
|
|
775
|
+
" ",
|
|
776
|
+
props.renderChild(ifClause, props.value, props.onChange)
|
|
777
|
+
]
|
|
778
|
+
}),
|
|
779
|
+
/* @__PURE__ */ jsx(Show, {
|
|
780
|
+
when: thenClause,
|
|
781
|
+
children: (then) => /* @__PURE__ */ jsxs("div", {
|
|
782
|
+
class: SC_CLASSES.conditionalThen,
|
|
783
|
+
children: [
|
|
784
|
+
/* @__PURE__ */ jsx("strong", { children: "then:" }),
|
|
785
|
+
" ",
|
|
786
|
+
props.renderChild(then(), props.value, props.onChange)
|
|
787
|
+
]
|
|
788
|
+
})
|
|
789
|
+
}),
|
|
790
|
+
/* @__PURE__ */ jsx(Show, {
|
|
791
|
+
when: elseClause,
|
|
792
|
+
children: (other) => /* @__PURE__ */ jsxs("div", {
|
|
793
|
+
class: SC_CLASSES.conditionalElse,
|
|
794
|
+
children: [
|
|
795
|
+
/* @__PURE__ */ jsx("strong", { children: "else:" }),
|
|
796
|
+
" ",
|
|
797
|
+
props.renderChild(other(), props.value, props.onChange)
|
|
798
|
+
]
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
]
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/** Render a negation field — JSON Schema `{ not: { ... } }`. */
|
|
805
|
+
function renderNegation(props) {
|
|
806
|
+
if (props.tree.type !== "negation") return null;
|
|
807
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
808
|
+
class: SC_CLASSES.negation,
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ jsx("strong", { children: "Must NOT match:" }),
|
|
811
|
+
" ",
|
|
812
|
+
props.renderChild(props.tree.negated, props.value, props.onChange)
|
|
813
|
+
]
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/** Headless renderer for `UnknownField` — JSON-encoded fallback. */
|
|
817
|
+
function renderUnknown(props) {
|
|
818
|
+
const id = inputId(props.path);
|
|
819
|
+
if (props.readOnly) {
|
|
820
|
+
if (props.value === void 0 || props.value === null) return /* @__PURE__ */ jsx("span", {
|
|
821
|
+
id,
|
|
822
|
+
children: "—"
|
|
823
|
+
});
|
|
824
|
+
return /* @__PURE__ */ jsx("span", {
|
|
825
|
+
id,
|
|
826
|
+
children: typeof props.value === "string" ? props.value : JSON.stringify(props.value)
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const strValue = typeof props.value === "string" ? props.value : "";
|
|
830
|
+
return /* @__PURE__ */ jsx("input", {
|
|
831
|
+
id,
|
|
832
|
+
type: "text",
|
|
833
|
+
value: props.writeOnly ? "" : strValue,
|
|
834
|
+
onInput: (e) => {
|
|
835
|
+
props.onChange(e.currentTarget.value);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderString, renderTuple, renderUnion, renderUnknown };
|