schema-components 2.0.1 → 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.
Files changed (196) hide show
  1. package/README.md +98 -1
  2. package/dist/SchemaComponent-B__6-5-E.d.mts +277 -0
  3. package/dist/SchemaComponent-BxzzsHsK.mjs +668 -0
  4. package/dist/adapter-ktQaheWB.d.mts +213 -0
  5. package/dist/constructorTypes-BdCiMS6e.d.mts +30 -0
  6. package/dist/core/adapter.d.mts +3 -213
  7. package/dist/core/adapter.mjs +33 -25
  8. package/dist/core/constraintHint.d.mts +1 -1
  9. package/dist/core/constraints.d.mts +2 -2
  10. package/dist/core/contexts.d.mts +71 -0
  11. package/dist/core/contexts.mjs +1 -0
  12. package/dist/core/diagnostics.d.mts +1 -1
  13. package/dist/core/errors.d.mts +1 -1
  14. package/dist/core/fieldOrder.d.mts +1 -1
  15. package/dist/{react → core}/fieldPath.d.mts +2 -2
  16. package/dist/{react → core}/fieldPath.mjs +3 -3
  17. package/dist/core/formats.d.mts +1 -1
  18. package/dist/core/guards.d.mts +2 -2
  19. package/dist/core/guards.mjs +2 -2
  20. package/dist/core/inferValue.d.mts +1 -1
  21. package/dist/core/limits.d.mts +1 -1
  22. package/dist/core/merge.d.mts +1 -1
  23. package/dist/core/normalise.d.mts +6 -6
  24. package/dist/core/normalise.mjs +1 -1
  25. package/dist/core/openapi30.d.mts +1 -1
  26. package/dist/core/openapi30.mjs +1 -1
  27. package/dist/core/ref.d.mts +1 -1
  28. package/dist/core/renderField.d.mts +147 -0
  29. package/dist/core/renderField.mjs +81 -0
  30. package/dist/core/renderer.d.mts +2 -199
  31. package/dist/core/swagger2.d.mts +1 -1
  32. package/dist/core/swagger2.mjs +1 -1
  33. package/dist/core/typeInference.d.mts +1 -981
  34. package/dist/core/types.d.mts +1 -1
  35. package/dist/core/unionMatch.d.mts +1 -1
  36. package/dist/core/uri.d.mts +2 -2
  37. package/dist/core/uri.mjs +2 -2
  38. package/dist/core/version.d.mts +1 -1
  39. package/dist/core/walkBuilders.d.mts +4 -4
  40. package/dist/core/walkBuilders.mjs +1 -1
  41. package/dist/core/walker.d.mts +1 -1
  42. package/dist/core/walker.mjs +3 -3
  43. package/dist/{errors-Dki7tji4.d.mts → errors-DbaI04x2.d.mts} +1 -1
  44. package/dist/html/a11y.d.mts +2 -2
  45. package/dist/html/html.d.mts +10 -8
  46. package/dist/html/renderToHtml.d.mts +5 -5
  47. package/dist/html/renderToHtml.mjs +45 -24
  48. package/dist/html/renderToHtmlStream.d.mts +5 -5
  49. package/dist/html/renderers.d.mts +1 -1
  50. package/dist/html/streamRenderers.d.mts +3 -3
  51. package/dist/{inferValue-PPXWJpbN.d.mts → inferValue-eAnh50EM.d.mts} +6 -6
  52. package/dist/lit/SchemaComponent.d.mts +125 -0
  53. package/dist/lit/SchemaComponent.mjs +2 -0
  54. package/dist/lit/SchemaField.d.mts +65 -0
  55. package/dist/lit/SchemaField.mjs +2 -0
  56. package/dist/lit/SchemaView.d.mts +14 -0
  57. package/dist/lit/SchemaView.mjs +2 -0
  58. package/dist/lit/constructorTypes.d.mts +2 -0
  59. package/dist/lit/constructorTypes.mjs +1 -0
  60. package/dist/lit/contexts.d.mts +78 -0
  61. package/dist/lit/contexts.mjs +238 -0
  62. package/dist/lit/defaultResolver.d.mts +33 -0
  63. package/dist/lit/defaultResolver.mjs +2 -0
  64. package/dist/lit/registry.d.mts +66 -0
  65. package/dist/lit/registry.mjs +2 -0
  66. package/dist/lit/renderers/baseElement.d.mts +131 -0
  67. package/dist/lit/renderers/baseElement.mjs +109 -0
  68. package/dist/lit/renderers/recordHelpers.d.mts +25 -0
  69. package/dist/lit/renderers/recordHelpers.mjs +55 -0
  70. package/dist/lit/renderers/scArray.d.mts +14 -0
  71. package/dist/lit/renderers/scArray.mjs +86 -0
  72. package/dist/lit/renderers/scBoolean.d.mts +15 -0
  73. package/dist/lit/renderers/scBoolean.mjs +47 -0
  74. package/dist/lit/renderers/scConditional.d.mts +23 -0
  75. package/dist/lit/renderers/scConditional.mjs +65 -0
  76. package/dist/lit/renderers/scDiscriminated.d.mts +23 -0
  77. package/dist/lit/renderers/scDiscriminated.mjs +138 -0
  78. package/dist/lit/renderers/scEnum.d.mts +16 -0
  79. package/dist/lit/renderers/scEnum.mjs +66 -0
  80. package/dist/lit/renderers/scFile.d.mts +15 -0
  81. package/dist/lit/renderers/scFile.mjs +53 -0
  82. package/dist/lit/renderers/scLiteralNullNever.d.mts +30 -0
  83. package/dist/lit/renderers/scLiteralNullNever.mjs +57 -0
  84. package/dist/lit/renderers/scNumber.d.mts +15 -0
  85. package/dist/lit/renderers/scNumber.mjs +64 -0
  86. package/dist/lit/renderers/scObject.d.mts +14 -0
  87. package/dist/lit/renderers/scObject.mjs +57 -0
  88. package/dist/lit/renderers/scRecord.d.mts +14 -0
  89. package/dist/lit/renderers/scRecord.mjs +112 -0
  90. package/dist/lit/renderers/scString.d.mts +19 -0
  91. package/dist/lit/renderers/scString.mjs +165 -0
  92. package/dist/lit/renderers/scTuple.d.mts +14 -0
  93. package/dist/lit/renderers/scTuple.mjs +58 -0
  94. package/dist/lit/renderers/scUnion.d.mts +14 -0
  95. package/dist/lit/renderers/scUnion.mjs +44 -0
  96. package/dist/lit/renderers/scUnknown.d.mts +15 -0
  97. package/dist/lit/renderers/scUnknown.mjs +45 -0
  98. package/dist/lit/ssr.d.mts +37 -0
  99. package/dist/lit/ssr.mjs +9565 -0
  100. package/dist/lit/types.d.mts +2 -0
  101. package/dist/lit/types.mjs +1 -0
  102. package/dist/lit/widget.d.mts +71 -0
  103. package/dist/lit/widget.mjs +87 -0
  104. package/dist/{normalise-DB-Xtjmn.mjs → normalise-BkePrJ4v.mjs} +6 -6
  105. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  106. package/dist/openapi/ApiLinks.d.mts +1 -1
  107. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  108. package/dist/openapi/ApiSecurity.d.mts +1 -1
  109. package/dist/openapi/components.d.mts +5 -5
  110. package/dist/openapi/components.mjs +1 -1
  111. package/dist/openapi/parser.d.mts +2 -2
  112. package/dist/openapi/resolve.d.mts +1 -1
  113. package/dist/openapi/resolve.mjs +1 -1
  114. package/dist/preact/SchemaComponent.d.mts +3 -0
  115. package/dist/preact/SchemaComponent.mjs +26 -0
  116. package/dist/preact/SchemaErrorBoundary.d.mts +2 -0
  117. package/dist/preact/SchemaErrorBoundary.mjs +20 -0
  118. package/dist/preact/SchemaView.d.mts +2 -0
  119. package/dist/preact/SchemaView.mjs +22 -0
  120. package/dist/preact/headless.d.mts +2 -0
  121. package/dist/preact/headless.mjs +18 -0
  122. package/dist/react/SchemaComponent.d.mts +3 -270
  123. package/dist/react/SchemaComponent.mjs +48 -39
  124. package/dist/react/SchemaErrorBoundary.mjs +7 -4
  125. package/dist/react/SchemaView.d.mts +11 -10
  126. package/dist/react/SchemaView.mjs +32 -29
  127. package/dist/react/a11y.d.mts +2 -2
  128. package/dist/react/fieldShell.d.mts +1 -1
  129. package/dist/react/headless.d.mts +1 -1
  130. package/dist/react/headlessRenderers.d.mts +2 -2
  131. package/dist/{ref-DdsbekXX.d.mts → ref-DWrQG1Er.d.mts} +1 -1
  132. package/dist/renderer-ab9E52Bp.d.mts +245 -0
  133. package/dist/solid/SchemaComponent.d.mts +136 -0
  134. package/dist/solid/SchemaComponent.mjs +391 -0
  135. package/dist/solid/SchemaErrorBoundary.d.mts +38 -0
  136. package/dist/solid/SchemaErrorBoundary.mjs +57 -0
  137. package/dist/solid/SchemaField.d.mts +40 -0
  138. package/dist/solid/SchemaField.mjs +113 -0
  139. package/dist/solid/SchemaView.d.mts +54 -0
  140. package/dist/solid/SchemaView.mjs +168 -0
  141. package/dist/solid/a11y.d.mts +70 -0
  142. package/dist/solid/a11y.mjs +71 -0
  143. package/dist/solid/contexts.d.mts +37 -0
  144. package/dist/solid/contexts.mjs +66 -0
  145. package/dist/solid/headless.d.mts +10 -0
  146. package/dist/solid/headless.mjs +27 -0
  147. package/dist/solid/renderers.d.mts +79 -0
  148. package/dist/solid/renderers.mjs +840 -0
  149. package/dist/solid/types.d.mts +90 -0
  150. package/dist/solid/types.mjs +1 -0
  151. package/dist/solid/widget.d.mts +29 -0
  152. package/dist/solid/widget.mjs +35 -0
  153. package/dist/themes/mantine.d.mts +1 -1
  154. package/dist/themes/mui.d.mts +1 -1
  155. package/dist/themes/radix.d.mts +1 -1
  156. package/dist/themes/shadcn.d.mts +1 -1
  157. package/dist/typeInference-Y8tNEQJk.d.mts +983 -0
  158. package/dist/types-BCy7K3nk.d.mts +125 -0
  159. package/package.json +71 -1
  160. package/src/svelte/SchemaComponent.svelte +427 -0
  161. package/src/svelte/SchemaErrorBoundary.svelte +66 -0
  162. package/src/svelte/SchemaField.svelte +216 -0
  163. package/src/svelte/SchemaProvider.svelte +46 -0
  164. package/src/svelte/SchemaView.svelte +244 -0
  165. package/src/svelte/a11y.ts +112 -0
  166. package/src/svelte/contexts.ts +79 -0
  167. package/src/svelte/dispatch.ts +267 -0
  168. package/src/svelte/headless.ts +73 -0
  169. package/src/svelte/headlessFns.ts +124 -0
  170. package/src/svelte/renderers/Array.svelte +98 -0
  171. package/src/svelte/renderers/Boolean.svelte +43 -0
  172. package/src/svelte/renderers/Conditional.svelte +67 -0
  173. package/src/svelte/renderers/DiscriminatedUnion.svelte +197 -0
  174. package/src/svelte/renderers/Enum.svelte +53 -0
  175. package/src/svelte/renderers/Fallback.svelte +24 -0
  176. package/src/svelte/renderers/File.svelte +46 -0
  177. package/src/svelte/renderers/Literal.svelte +29 -0
  178. package/src/svelte/renderers/Mount.svelte +24 -0
  179. package/src/svelte/renderers/Negation.svelte +35 -0
  180. package/src/svelte/renderers/Never.svelte +24 -0
  181. package/src/svelte/renderers/Null.svelte +19 -0
  182. package/src/svelte/renderers/Number.svelte +68 -0
  183. package/src/svelte/renderers/Object.svelte +74 -0
  184. package/src/svelte/renderers/Record.svelte +134 -0
  185. package/src/svelte/renderers/RecursionSentinel.svelte +27 -0
  186. package/src/svelte/renderers/String.svelte +152 -0
  187. package/src/svelte/renderers/Tuple.svelte +84 -0
  188. package/src/svelte/renderers/Union.svelte +49 -0
  189. package/src/svelte/renderers/Unknown.svelte +42 -0
  190. package/src/svelte/svelte-modules.d.ts +25 -0
  191. package/src/svelte/types.ts +238 -0
  192. package/src/svelte/widget.ts +62 -0
  193. /package/dist/{diagnostics-BTrm3O6J.d.mts → diagnostics-mftUZI7c.d.mts} +0 -0
  194. /package/dist/{limits-x4OiyJxh.d.mts → limits-Vv9hUbI_.d.mts} +0 -0
  195. /package/dist/{types-BrYbjC7_.d.mts → types-BBQaEPfE.d.mts} +0 -0
  196. /package/dist/{version-DL8U5RuA.d.mts → version-BEBx10ND.d.mts} +0 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Svelte 5 implementation of `core/contexts.ts`'s {@link ContextPort}
3
+ * for the resolver and widget registry.
4
+ *
5
+ * Wires the abstract provide / consume port to Svelte's native
6
+ * `setContext()` / `getContext()` primitives (re-exported from
7
+ * `svelte`). Each port is keyed by a `Symbol` so multiple framework
8
+ * adapters can coexist in the same module graph without aliasing —
9
+ * Svelte's context keys are object-identity-compared.
10
+ *
11
+ * Unlike the React `<Provider>` wrapper, Svelte's context model is
12
+ * "set on the current component instance, read from any descendant" —
13
+ * the {@link ContextPort.provide} call therefore does not wrap
14
+ * `children`. Instead, it sets the value on the component currently
15
+ * being initialised; descendants call {@link ContextPort.consume} to
16
+ * read it. The `children` argument is accepted purely to satisfy the
17
+ * port contract; it is returned unchanged so callers that pass a
18
+ * snippet or other ad-hoc representation can still receive their
19
+ * input back.
20
+ *
21
+ * @group Framework Adapters
22
+ */
23
+
24
+ import { setContext, getContext } from "svelte";
25
+ import type { ContextPort } from "../core/contexts.ts";
26
+ import type { SvelteComponentResolver, SvelteWidgetMap } from "./types.ts";
27
+
28
+ /**
29
+ * Symbol key for the resolver context. Identity-compared by Svelte's
30
+ * context API — never aliases against the widget context or any other
31
+ * port.
32
+ */
33
+ const RESOLVER_CONTEXT_KEY = Symbol("schema-components/svelte:resolver");
34
+
35
+ /**
36
+ * Symbol key for the widgets context. Sibling to
37
+ * {@link RESOLVER_CONTEXT_KEY}.
38
+ */
39
+ const WIDGETS_CONTEXT_KEY = Symbol("schema-components/svelte:widgets");
40
+
41
+ /**
42
+ * Context port for the active {@link SvelteComponentResolver}.
43
+ *
44
+ * The {@link ContextPort.consume} side returns `undefined` when no
45
+ * resolver has been provided in the current component subtree — the
46
+ * Svelte dispatcher then falls through to the headless resolver,
47
+ * matching the React adapter's behaviour with its
48
+ * `UserResolverContext` default value.
49
+ */
50
+ export const resolverContext: ContextPort<SvelteComponentResolver | undefined> =
51
+ {
52
+ provide(value, children) {
53
+ setContext(RESOLVER_CONTEXT_KEY, value);
54
+ return children;
55
+ },
56
+ consume() {
57
+ return getContext<SvelteComponentResolver | undefined>(
58
+ RESOLVER_CONTEXT_KEY
59
+ );
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Context port for the active {@link SvelteWidgetMap}.
65
+ *
66
+ * The {@link ContextPort.consume} side returns `undefined` when no
67
+ * widget map has been provided — the dispatcher then falls through to
68
+ * the per-instance and global widget registries before consulting the
69
+ * resolver chain.
70
+ */
71
+ export const widgetsContext: ContextPort<SvelteWidgetMap | undefined> = {
72
+ provide(value, children) {
73
+ setContext(WIDGETS_CONTEXT_KEY, value);
74
+ return children;
75
+ },
76
+ consume() {
77
+ return getContext<SvelteWidgetMap | undefined>(WIDGETS_CONTEXT_KEY);
78
+ },
79
+ };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Svelte-flavoured wrappers around `core/renderField.ts`'s
3
+ * framework-agnostic dispatcher.
4
+ *
5
+ * The dispatcher is the same one used by the React adapter — see
6
+ * `core/renderField.ts`. This module supplies the Svelte-specific
7
+ * {@link "../core/renderField.ts".DispatchConfig | DispatchConfig}:
8
+ * the `buildProps` factory, the `lookupRenderFn` against a
9
+ * {@link SvelteComponentResolver}, the per-step result coercion,
10
+ * and the recursion-cap sentinel.
11
+ *
12
+ * Centralising the wiring lets both `SchemaComponent.svelte` and
13
+ * `SchemaView.svelte` plug into the dispatcher without duplicating
14
+ * the closures, and keeps the dispatch order (depth cap → widget →
15
+ * resolver → fallback) identical across editable and read-only call
16
+ * sites.
17
+ */
18
+
19
+ import {
20
+ buildRenderProps,
21
+ RESOLVER_KEYS,
22
+ type RenderProps,
23
+ } from "../core/renderer.ts";
24
+ import { dispatchRenderField } from "../core/renderField.ts";
25
+ import { SchemaRenderError } from "../core/errors.ts";
26
+ import type { WalkedField } from "../core/types.ts";
27
+ import { headlessSvelteResolver } from "./headless.ts";
28
+ import { lookupGlobalWidget } from "./widget.ts";
29
+ import type {
30
+ SvelteComponentConstructor,
31
+ SvelteComponentResolver,
32
+ SvelteRenderDescriptor,
33
+ SvelteRenderProps,
34
+ SvelteWidgetMap,
35
+ } from "./types.ts";
36
+
37
+ /**
38
+ * Merge a user-supplied {@link SvelteComponentResolver} on top of
39
+ * the headless resolver. Mirrors `core/renderer.ts`'s
40
+ * `mergeResolvers` for the Svelte-flavoured resolver shape — user
41
+ * values win, fallback fills gaps.
42
+ *
43
+ * Implemented directly rather than delegating to the generic
44
+ * `mergeResolvers` because the latter is typed against
45
+ * `ComponentResolver` — the React-flavoured resolver whose values
46
+ * are typed `RenderFunction\<unknown, RenderProps\>`. The per-key
47
+ * behaviour is identical: each
48
+ * {@link RESOLVER_KEYS} entry is taken from `user` if defined,
49
+ * otherwise from `fallback`. The single source of truth on which
50
+ * keys exist is {@link RESOLVER_KEYS}, so adding a new
51
+ * `WalkedField` variant in `core/types.ts` automatically threads
52
+ * through the Svelte merge.
53
+ */
54
+ export function mergeSvelteResolvers(
55
+ user: SvelteComponentResolver,
56
+ fallback: SvelteComponentResolver
57
+ ): SvelteComponentResolver {
58
+ const merged: SvelteComponentResolver = {};
59
+ for (const key of RESOLVER_KEYS) {
60
+ const fn = user[key] ?? fallback[key];
61
+ if (fn !== undefined) {
62
+ merged[key] = fn;
63
+ }
64
+ }
65
+ return merged;
66
+ }
67
+
68
+ /**
69
+ * Dispatch a single field through the Svelte dispatch chain.
70
+ *
71
+ * Looks up the matching {@link SvelteRenderFunction} in the supplied
72
+ * resolver (with the headless resolver as fallback), invokes it with
73
+ * the per-field props, and returns the resulting
74
+ * {@link SvelteRenderDescriptor} for the parent renderer to mount.
75
+ *
76
+ * Widget overrides (`.meta({ component: name })`) are resolved
77
+ * against the supplied instance → context → global chain before the
78
+ * resolver. The dispatch order matches the React adapter exactly:
79
+ *
80
+ * 1. Depth cap (`MAX_RENDER_DEPTH`) — returns the recursion
81
+ * sentinel descriptor.
82
+ * 2. Widget override — instance map → context map → global
83
+ * registry.
84
+ * 3. Resolver render function for `tree.type`.
85
+ * 4. Fallback — emits an em-dash / stringified-value descriptor.
86
+ */
87
+ export function renderFieldSvelte(
88
+ tree: WalkedField,
89
+ value: unknown,
90
+ onChange: (v: unknown) => void,
91
+ userResolver: SvelteComponentResolver | undefined,
92
+ renderChild: SvelteRenderProps["renderChild"],
93
+ path: string,
94
+ instanceWidgets: SvelteWidgetMap | undefined,
95
+ contextWidgets: SvelteWidgetMap | undefined,
96
+ depth: number,
97
+ fallbackComponent: SvelteComponentConstructor,
98
+ sentinelComponent: SvelteComponentConstructor
99
+ ): SvelteRenderDescriptor | null {
100
+ if (path.length === 0) {
101
+ throw new Error(
102
+ "renderFieldSvelte requires a non-empty path. Pass the root " +
103
+ "path for the root field and use renderChild's pathSuffix " +
104
+ "to derive child paths."
105
+ );
106
+ }
107
+
108
+ const resolver: SvelteComponentResolver =
109
+ userResolver !== undefined
110
+ ? mergeSvelteResolvers(userResolver, headlessSvelteResolver)
111
+ : headlessSvelteResolver;
112
+
113
+ return dispatchRenderField<
114
+ SvelteRenderProps,
115
+ SvelteRenderDescriptor | null,
116
+ SvelteComponentResolver
117
+ >({
118
+ tree,
119
+ value,
120
+ path,
121
+ depth,
122
+ resolver,
123
+ config: {
124
+ buildProps: (fieldTree, fieldPath) => {
125
+ // The React-flavoured `buildRenderProps` returns
126
+ // `RenderProps`. Svelte's `SvelteRenderProps` shares
127
+ // every field except the `renderChild` return type
128
+ // (which is `SvelteRenderDescriptor | null` here vs.
129
+ // `unknown` for React). Adapt by re-wrapping the
130
+ // child render function we hold to satisfy the
131
+ // structural shape `buildRenderProps` expects, then
132
+ // re-cast on output. The cast is contained to this
133
+ // one boundary; no runtime reshaping required.
134
+ const reactShapedChild: RenderProps["renderChild"] = (
135
+ childTree,
136
+ childValue,
137
+ childOnChange,
138
+ pathSuffix
139
+ ) =>
140
+ renderChild(
141
+ childTree,
142
+ childValue,
143
+ childOnChange,
144
+ pathSuffix
145
+ );
146
+ const reactProps = buildRenderProps(
147
+ fieldTree,
148
+ value,
149
+ onChange,
150
+ reactShapedChild,
151
+ fieldPath
152
+ );
153
+ // Re-shape onto SvelteRenderProps. The structural
154
+ // overlap with `RenderProps` is total apart from the
155
+ // `renderChild` return type; we substitute the
156
+ // Svelte-shaped function so consumers receive the
157
+ // proper output narrowing.
158
+ const svelteProps: SvelteRenderProps = {
159
+ ...reactProps,
160
+ renderChild,
161
+ };
162
+ return svelteProps;
163
+ },
164
+ lookupRenderFn: (type, mergedResolver) => mergedResolver[type],
165
+ ...(instanceWidgets !== undefined || contextWidgets !== undefined
166
+ ? {
167
+ lookupWidget: (name: string) =>
168
+ instanceWidgets?.get(name) ??
169
+ contextWidgets?.get(name) ??
170
+ lookupGlobalWidget(name),
171
+ }
172
+ : {
173
+ lookupWidget: (name: string) => lookupGlobalWidget(name),
174
+ }),
175
+ recursionSentinel: (fieldTree) => ({
176
+ component: sentinelComponent,
177
+ props: buildSentinelProps(
178
+ fieldTree,
179
+ value,
180
+ onChange,
181
+ path,
182
+ renderChild
183
+ ),
184
+ }),
185
+ fallback: (fieldTree, fieldValue) => ({
186
+ component: fallbackComponent,
187
+ props: buildSentinelProps(
188
+ fieldTree,
189
+ fieldValue,
190
+ onChange,
191
+ path,
192
+ renderChild
193
+ ),
194
+ }),
195
+ coerceResult: (result, step) => {
196
+ // Widget step — undefined / null falls through to
197
+ // the resolver. Anything else is treated as a
198
+ // descriptor (or null short-circuit).
199
+ if (step === "widget") {
200
+ if (result === undefined || result === null)
201
+ return undefined;
202
+ if (isDescriptor(result)) return result;
203
+ return undefined;
204
+ }
205
+ // Resolver step — undefined falls through to the
206
+ // fallback; null short-circuits with "render
207
+ // nothing" (matches React's empty-array suppression
208
+ // path).
209
+ if (result === undefined) return undefined;
210
+ if (result === null) return null;
211
+ if (isDescriptor(result)) return result;
212
+ return undefined;
213
+ },
214
+ wrapRenderError: (err, fieldTree, fieldPath) =>
215
+ new SchemaRenderError(
216
+ err instanceof Error
217
+ ? err.message
218
+ : `Render function threw for type "${fieldTree.type}" at "${fieldPath}"`,
219
+ fieldTree,
220
+ fieldTree.type,
221
+ err
222
+ ),
223
+ },
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Build the per-field props passed to the recursion-sentinel /
229
+ * fallback Svelte components. Both stages need the full
230
+ * {@link SvelteRenderProps} shape (so the component can read
231
+ * `value`, `meta`, etc.) so the helper centralises the construction.
232
+ */
233
+ function buildSentinelProps(
234
+ tree: WalkedField,
235
+ value: unknown,
236
+ onChange: (v: unknown) => void,
237
+ path: string,
238
+ renderChild: SvelteRenderProps["renderChild"]
239
+ ): SvelteRenderProps {
240
+ return {
241
+ value,
242
+ readOnly: tree.editability === "presentation",
243
+ writeOnly: tree.editability === "input",
244
+ meta: tree.meta,
245
+ constraints: tree.constraints,
246
+ path,
247
+ tree,
248
+ onChange,
249
+ renderChild,
250
+ ...(tree.examples !== undefined ? { examples: tree.examples } : {}),
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Type guard narrowing an unknown widget / resolver return value to
256
+ * a {@link SvelteRenderDescriptor}. The shape check is structural:
257
+ * any object with a callable `component` and a `props` field passes.
258
+ *
259
+ * Strict enough that an accidentally-returned `ReactNode` or HTML
260
+ * string is rejected before the dispatcher hands it to `<Mount>`.
261
+ */
262
+ function isDescriptor(value: unknown): value is SvelteRenderDescriptor {
263
+ if (typeof value !== "object" || value === null) return false;
264
+ if (!("component" in value)) return false;
265
+ if (!("props" in value)) return false;
266
+ return typeof value.component === "function";
267
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Svelte 5 headless renderer — the default
3
+ * {@link SvelteComponentResolver} implementation.
4
+ *
5
+ * Produces plain HTML elements (through one `.svelte` component per
6
+ * schema type) for every field variant the walker can emit. Theme
7
+ * adapters override entries by supplying their own resolver to the
8
+ * `<SchemaProvider>` component.
9
+ *
10
+ * The headless resolver wires each entry through
11
+ * {@link makeSvelteRenderer} — which pairs the component constructor
12
+ * with the per-field props the dispatcher computes, returning a
13
+ * {@link SvelteRenderDescriptor} that the parent container renderer
14
+ * mounts via `<Mount descriptor={…} />`.
15
+ *
16
+ * Mirror of `react/headless.tsx` — same schema-type coverage, same
17
+ * accessibility wiring, same fallback policy. Every `WalkedField`
18
+ * variant the walker can emit has a registered renderer here;
19
+ * missing a registration would cause `getRenderFunction` to return
20
+ * `undefined` and the field to render as nothing, so the resolver
21
+ * registration below is the single source of completeness.
22
+ */
23
+
24
+ import { makeSvelteRenderer } from "./types.ts";
25
+ import type { SvelteComponentResolver } from "./types.ts";
26
+ import StringSvelte from "./renderers/String.svelte";
27
+ import NumberSvelte from "./renderers/Number.svelte";
28
+ import BooleanSvelte from "./renderers/Boolean.svelte";
29
+ import EnumSvelte from "./renderers/Enum.svelte";
30
+ import ObjectSvelte from "./renderers/Object.svelte";
31
+ import ArraySvelte from "./renderers/Array.svelte";
32
+ import TupleSvelte from "./renderers/Tuple.svelte";
33
+ import RecordSvelte from "./renderers/Record.svelte";
34
+ import UnionSvelte from "./renderers/Union.svelte";
35
+ import DiscriminatedUnionSvelte from "./renderers/DiscriminatedUnion.svelte";
36
+ import LiteralSvelte from "./renderers/Literal.svelte";
37
+ import NullSvelte from "./renderers/Null.svelte";
38
+ import NeverSvelte from "./renderers/Never.svelte";
39
+ import ConditionalSvelte from "./renderers/Conditional.svelte";
40
+ import NegationSvelte from "./renderers/Negation.svelte";
41
+ import FileSvelte from "./renderers/File.svelte";
42
+ import UnknownSvelte from "./renderers/Unknown.svelte";
43
+
44
+ /**
45
+ * Default {@link SvelteComponentResolver} used by `<SchemaComponent>`
46
+ * / `<SchemaView>` when no theme adapter is provided via
47
+ * `<SchemaProvider>`.
48
+ *
49
+ * Each entry pairs a schema type with the Svelte component
50
+ * constructor that should render it. {@link makeSvelteRenderer}
51
+ * wraps the constructor into the
52
+ * `(props) =\> SvelteRenderDescriptor` shape consumed by the
53
+ * dispatcher.
54
+ */
55
+ export const headlessSvelteResolver: SvelteComponentResolver = {
56
+ string: makeSvelteRenderer(StringSvelte),
57
+ number: makeSvelteRenderer(NumberSvelte),
58
+ boolean: makeSvelteRenderer(BooleanSvelte),
59
+ null: makeSvelteRenderer(NullSvelte),
60
+ enum: makeSvelteRenderer(EnumSvelte),
61
+ object: makeSvelteRenderer(ObjectSvelte),
62
+ array: makeSvelteRenderer(ArraySvelte),
63
+ tuple: makeSvelteRenderer(TupleSvelte),
64
+ record: makeSvelteRenderer(RecordSvelte),
65
+ union: makeSvelteRenderer(UnionSvelte),
66
+ discriminatedUnion: makeSvelteRenderer(DiscriminatedUnionSvelte),
67
+ conditional: makeSvelteRenderer(ConditionalSvelte),
68
+ negation: makeSvelteRenderer(NegationSvelte),
69
+ literal: makeSvelteRenderer(LiteralSvelte),
70
+ file: makeSvelteRenderer(FileSvelte),
71
+ never: makeSvelteRenderer(NeverSvelte),
72
+ unknown: makeSvelteRenderer(UnknownSvelte),
73
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pure helper functions shared by the Svelte 5 headless renderers.
3
+ *
4
+ * Ports the framework-agnostic helpers attached to
5
+ * `react/headlessRenderers.tsx`:
6
+ *
7
+ * - {@link defaultRecordValue} — type-appropriate "new entry" value
8
+ * for `RecordField` / `ArrayField`'s Add button.
9
+ * - {@link nextRecordKey} — collision-free key generator for the
10
+ * Record "Add entry" button.
11
+ * - {@link renameRecordKey} — insertion-order-preserving rename.
12
+ * - {@link discriminatedUnionValueForTab} — tab-index → emitted
13
+ * value mapper for the discriminated-union tabs widget.
14
+ *
15
+ * These contain no rendering logic — they are pure functions over
16
+ * the walked field tree and JS values, so the React and Svelte
17
+ * adapters share the same behaviour without depending on each
18
+ * other's rendering surface.
19
+ */
20
+
21
+ import type { WalkedField } from "../core/types.ts";
22
+
23
+ /**
24
+ * Compute the default value for a freshly added record / array
25
+ * entry based on the value-type schema. Falls back to a
26
+ * type-appropriate empty value when the schema does not declare a
27
+ * default.
28
+ *
29
+ * The switch is exhaustive over `WalkedField.type` so a new schema
30
+ * variant added to the walker forces a deliberate choice of default
31
+ * here rather than silently producing `undefined`.
32
+ */
33
+ export function defaultRecordValue(valueType: WalkedField): unknown {
34
+ if (valueType.defaultValue !== undefined) return valueType.defaultValue;
35
+ switch (valueType.type) {
36
+ case "string":
37
+ return "";
38
+ case "number":
39
+ return 0;
40
+ case "boolean":
41
+ return false;
42
+ case "array":
43
+ return [];
44
+ case "object":
45
+ case "record":
46
+ return {};
47
+ case "null":
48
+ return null;
49
+ case "unknown":
50
+ case "enum":
51
+ case "literal":
52
+ case "tuple":
53
+ case "union":
54
+ case "discriminatedUnion":
55
+ case "conditional":
56
+ case "negation":
57
+ case "file":
58
+ case "never":
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Generate a unique, currently-unused key for a new record entry.
65
+ * Picks the first of `key`, `key-1`, `key-2`, … that is not in
66
+ * `existing`.
67
+ */
68
+ export function nextRecordKey(
69
+ existing: readonly string[],
70
+ base = "key"
71
+ ): string {
72
+ if (!existing.includes(base)) return base;
73
+ let i = 1;
74
+ while (existing.includes(`${base}-${String(i)}`)) i += 1;
75
+ return `${base}-${String(i)}`;
76
+ }
77
+
78
+ /**
79
+ * Rename a key in an object while preserving insertion order.
80
+ * Returns the original object reference when the rename is a no-op
81
+ * (`oldKey === newKey`) or when `newKey` collides with an existing
82
+ * key — the renderer uses reference equality to skip an unnecessary
83
+ * `props.onChange` call.
84
+ */
85
+ export function renameRecordKey(
86
+ obj: Record<string, unknown>,
87
+ oldKey: string,
88
+ newKey: string
89
+ ): Record<string, unknown> {
90
+ if (oldKey === newKey) return obj;
91
+ if (newKey in obj && newKey !== oldKey) return obj;
92
+ const renamed: Record<string, unknown> = {};
93
+ for (const [k, v] of Object.entries(obj)) {
94
+ renamed[k === oldKey ? newKey : k] = v;
95
+ }
96
+ return renamed;
97
+ }
98
+
99
+ /**
100
+ * Pure helper: convert a tab index into the new value the
101
+ * discriminated union should emit. Returns `undefined` when the index
102
+ * is out of bounds — callers skip the `onChange` call entirely.
103
+ *
104
+ * Extracted so the contract is unit-testable without rendering the
105
+ * tabs component.
106
+ */
107
+ export function discriminatedUnionValueForTab(
108
+ optionLabels: readonly string[],
109
+ discKey: string,
110
+ newIndex: number
111
+ ): Record<string, string> | undefined {
112
+ const label = optionLabels[newIndex];
113
+ if (label === undefined) return undefined;
114
+ return { [discKey]: label };
115
+ }
116
+
117
+ /**
118
+ * Wrap an index into a valid tab index using floored modulo. Used
119
+ * by the discriminated-union keyboard handler to wrap arrow-key
120
+ * navigation at the extremes.
121
+ */
122
+ export function wrapTabIndex(index: number, total: number): number {
123
+ return ((index % total) + total) % total;
124
+ }
@@ -0,0 +1,98 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `ArrayField` — ordered list with
3
+ add / remove controls. Mirror of
4
+ `react/headlessRenderers.tsx :: renderArray`.
5
+
6
+ Read-only mode renders the list without controls; an empty array
7
+ produces no list so leaf nodes in recursive schemas don't get
8
+ orphaned "Children" labels.
9
+
10
+ Editable mode wraps each item in `<li>` with a Remove button and
11
+ appends an "Add item" button at the foot. `<button type="button">`
12
+ keeps the controls keyboard-accessible (Space / Enter) without
13
+ custom key handlers and stops them accidentally submitting any
14
+ enclosing form.
15
+ -->
16
+ <script lang="ts">
17
+ import type { SvelteRenderProps } from "../types.ts";
18
+ import { ariaLabel } from "../a11y.ts";
19
+ import { defaultRecordValue } from "../headlessFns.ts";
20
+ import Mount from "./Mount.svelte";
21
+
22
+ const props = $props<SvelteRenderProps>();
23
+
24
+ const arr = $derived<unknown[]>(
25
+ Array.isArray(props.value) ? props.value : []
26
+ );
27
+
28
+ const element = $derived(
29
+ props.tree.type === "array" ? props.tree.element : undefined
30
+ );
31
+ </script>
32
+
33
+ {#if props.tree.type !== "array" || element === undefined}
34
+ {#if !props.readOnly}
35
+ <!-- defensive: misconfigured resolver wired this component into a non-array slot. -->
36
+ {/if}
37
+ {:else if props.readOnly}
38
+ {#if arr.length > 0}
39
+ <ul role="group" aria-label={ariaLabel(props.meta.description)}>
40
+ {#each arr as item, i (i)}
41
+ {@const child = props.renderChild(
42
+ element,
43
+ item,
44
+ () => {
45
+ /* read-only: noop */
46
+ },
47
+ `[${String(i)}]`
48
+ )}
49
+ {#if child !== null}
50
+ <li>
51
+ <Mount descriptor={child} />
52
+ </li>
53
+ {/if}
54
+ {/each}
55
+ </ul>
56
+ {/if}
57
+ {:else}
58
+ {@const handleRemove = (index: number) => {
59
+ const next = arr.slice();
60
+ next.splice(index, 1);
61
+ props.onChange(next);
62
+ }}
63
+ {@const handleAdd = () => {
64
+ const next = arr.slice();
65
+ next.push(defaultRecordValue(element));
66
+ props.onChange(next);
67
+ }}
68
+ <div role="group" aria-label={ariaLabel(props.meta.description)}>
69
+ <ul>
70
+ {#each arr as item, i (i)}
71
+ {@const childOnChange = (v: unknown) => {
72
+ const nextArr = arr.slice();
73
+ nextArr[i] = v;
74
+ props.onChange(nextArr);
75
+ }}
76
+ {@const child = props.renderChild(
77
+ element,
78
+ item,
79
+ childOnChange,
80
+ `[${String(i)}]`
81
+ )}
82
+ <li>
83
+ {#if child !== null}
84
+ <Mount descriptor={child} />
85
+ {/if}
86
+ <button
87
+ type="button"
88
+ aria-label={`Remove item ${String(i)}`}
89
+ onclick={() => handleRemove(i)}>Remove</button
90
+ >
91
+ </li>
92
+ {/each}
93
+ </ul>
94
+ <button type="button" aria-label="Add item" onclick={handleAdd}
95
+ >Add</button
96
+ >
97
+ </div>
98
+ {/if}
@@ -0,0 +1,43 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `BooleanField` — plain
3
+ `<input type="checkbox">` mirror of
4
+ `react/headlessRenderers.tsx :: renderBoolean`.
5
+
6
+ Read-only mode renders "Yes" / "No" through a `<span>`, falling
7
+ back to an em-dash for non-boolean values. Editable mode applies
8
+ the description as an `aria-label` via `buildAriaAttrs` — there
9
+ is no separate placeholder slot on a checkbox, so the description
10
+ drives the accessible name instead.
11
+ -->
12
+ <script lang="ts">
13
+ import type { SvelteRenderProps } from "../types.ts";
14
+ import { fieldDomId } from "../../core/idPath.ts";
15
+ import { EM_DASH } from "../../core/cssClasses.ts";
16
+ import { buildAriaAttrs } from "../a11y.ts";
17
+
18
+ const props = $props<SvelteRenderProps>();
19
+
20
+ const id = $derived(fieldDomId(props.path));
21
+ </script>
22
+
23
+ {#if props.readOnly}
24
+ {#if typeof props.value !== "boolean"}
25
+ <span {id}>{EM_DASH}</span>
26
+ {:else}
27
+ <span {id}>{props.value ? "Yes" : "No"}</span>
28
+ {/if}
29
+ {:else}
30
+ {@const ariaAttrs = buildAriaAttrs(props.tree, props.meta.description)}
31
+ <input
32
+ {id}
33
+ type="checkbox"
34
+ checked={props.writeOnly ? false : props.value === true}
35
+ onchange={(e) => {
36
+ const target = e.currentTarget;
37
+ if (target instanceof HTMLInputElement) {
38
+ props.onChange(target.checked);
39
+ }
40
+ }}
41
+ {...ariaAttrs}
42
+ />
43
+ {/if}