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,66 @@
1
+ <!--
2
+ Svelte 5 error boundary for schema-components.
3
+
4
+ Uses Svelte 5's native `<svelte:boundary>` primitive (introduced
5
+ in Svelte 5) — the structural equivalent of the React class
6
+ component `componentDidCatch` / `getDerivedStateFromError`
7
+ pattern from `react/SchemaErrorBoundary.tsx`.
8
+
9
+ Catches synchronous render errors thrown by descendant
10
+ components (including `<SchemaComponent>` / `<SchemaView>` and
11
+ any custom theme adapter renderers). The boundary surfaces the
12
+ caught `Error` to the supplied `fallback` snippet alongside a
13
+ `reset` callback that clears the error state — Svelte's boundary
14
+ primitive owns the state internally; the snippet just receives
15
+ whichever helpers Svelte passes to it.
16
+
17
+ Mirrors the React boundary's documentation: this primitive does
18
+ NOT catch errors thrown from event handlers, async work, or
19
+ server-side rendering. Those must be handled at the host
20
+ application boundary.
21
+ -->
22
+ <script lang="ts">
23
+ import type { Snippet } from "svelte";
24
+ import { SchemaError } from "../core/errors.ts";
25
+
26
+ interface Props {
27
+ /** Rendered while no error is in flight. */
28
+ children: Snippet;
29
+ /**
30
+ * Rendered when the boundary catches an error. Receives the
31
+ * thrown `Error` and a `reset` thunk that re-renders the
32
+ * children once the underlying problem is fixed (e.g. a
33
+ * corrected `schema` prop).
34
+ */
35
+ fallback: Snippet<[Error, () => void]>;
36
+ }
37
+
38
+ const { children, fallback }: Props = $props();
39
+
40
+ function logIfUnexpected(error: unknown): void {
41
+ // Non-SchemaError thrown values are forwarded to the
42
+ // console so the application has at least one signal in the
43
+ // logs. SchemaError instances are presumed handled by the
44
+ // `onError` callback on `<SchemaComponent>` / `<SchemaView>`.
45
+ if (!(error instanceof SchemaError)) {
46
+ // Diagnostic surface mirroring the React adapter — the
47
+ // boundary's contract is "log the unexpected, surface
48
+ // structured errors through `failed`".
49
+ console.error("[schema-components] Unhandled render error:", error);
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <svelte:boundary
55
+ onerror={(error) => {
56
+ logIfUnexpected(error);
57
+ }}
58
+ >
59
+ {@render children()}
60
+ {#snippet failed(error, reset)}
61
+ {@render fallback(
62
+ error instanceof Error ? error : new Error(String(error)),
63
+ reset
64
+ )}
65
+ {/snippet}
66
+ </svelte:boundary>
@@ -0,0 +1,216 @@
1
+ <!--
2
+ Render a single field from a schema by dot-separated `path`.
3
+ Mirror of `react/SchemaComponent.tsx :: SchemaField`.
4
+
5
+ Walks the full schema tree, resolves the field at `path`, and
6
+ renders only that subtree. Useful for embedding individual
7
+ fields inside bespoke layouts when `<SchemaComponent>` would
8
+ render too much.
9
+ -->
10
+ <script lang="ts" generics="T = unknown, Ref extends string | undefined = undefined">
11
+ import { walk } from "../core/walker.ts";
12
+ import type { WalkOptions } from "../core/walkBuilders.ts";
13
+ import {
14
+ normaliseSchema,
15
+ type SchemaIoSide,
16
+ } from "../core/adapter.ts";
17
+ import {
18
+ SchemaNormalisationError,
19
+ SchemaFieldError,
20
+ } from "../core/errors.ts";
21
+ import type { SchemaMeta, WalkedField } from "../core/types.ts";
22
+ import {
23
+ resolvePath,
24
+ resolveValue,
25
+ setNestedValue,
26
+ } from "../core/fieldPath.ts";
27
+ import { resolverContext, widgetsContext } from "./contexts.ts";
28
+ import { renderFieldSvelte } from "./dispatch.ts";
29
+ import RecursionSentinel from "./renderers/RecursionSentinel.svelte";
30
+ import Fallback from "./renderers/Fallback.svelte";
31
+ import Mount from "./renderers/Mount.svelte";
32
+ import type {
33
+ SvelteRenderDescriptor,
34
+ SvelteRenderProps,
35
+ } from "./types.ts";
36
+
37
+ interface Props {
38
+ /** Dot-separated path (e.g. "address.city"). */
39
+ path: string;
40
+ /** The schema to extract the field from. */
41
+ schema: T;
42
+ /** OpenAPI ref string. */
43
+ ref?: Ref;
44
+ /** Direction (`"output"` / `"input"`) for codec / transform schemas. */
45
+ io?: SchemaIoSide;
46
+ /** Current value of the root schema. */
47
+ value?: unknown;
48
+ /** Called with the updated root value when this field changes. */
49
+ onChange?: (value: unknown) => void;
50
+ /** Override meta for this specific field. */
51
+ meta?: SchemaMeta;
52
+ /**
53
+ * Prefix used for every input `id` / label `for` in this
54
+ * subtree. Defaults to a sanitised, mount-stable counter.
55
+ */
56
+ idPrefix?: string;
57
+ }
58
+
59
+ const {
60
+ path,
61
+ schema,
62
+ ref,
63
+ io,
64
+ value,
65
+ onChange,
66
+ meta: fieldMeta,
67
+ idPrefix,
68
+ }: Props = $props();
69
+
70
+ const userResolver = resolverContext.consume();
71
+ const contextWidgets = widgetsContext.consume();
72
+
73
+ const fallbackPrefix = `sc-svelte-field-${String(nextFieldInstanceId())}`;
74
+ const rootPath = $derived(joinPath(idPrefix ?? fallbackPrefix, path));
75
+
76
+ interface NormalisedShape {
77
+ jsonSchema: Record<string, unknown>;
78
+ rootMeta: SchemaMeta | undefined;
79
+ rootDocument: Record<string, unknown>;
80
+ }
81
+
82
+ const normalisedResult = $derived<NormalisedShape>(
83
+ normaliseOrThrow(schema, ref, io)
84
+ );
85
+
86
+ const walkOptions = $derived<WalkOptions>({
87
+ ...(fieldMeta !== undefined ? { componentMeta: fieldMeta } : {}),
88
+ ...(normalisedResult.rootMeta !== undefined
89
+ ? { rootMeta: normalisedResult.rootMeta }
90
+ : {}),
91
+ rootDocument: normalisedResult.rootDocument,
92
+ });
93
+
94
+ const fullTree = $derived(walk(normalisedResult.jsonSchema, walkOptions));
95
+
96
+ const fieldTree = $derived<WalkedField>(
97
+ resolvePathOrThrow(fullTree, path, schema)
98
+ );
99
+
100
+ const fieldValue = $derived(resolveValue(value, path));
101
+
102
+ function handleChange(nextFieldValue: unknown): void {
103
+ const newRootValue = setNestedValue(value, path, nextFieldValue);
104
+ onChange?.(newRootValue);
105
+ }
106
+
107
+ function makeRenderChild(
108
+ currentDepth: number,
109
+ parentPath: string
110
+ ): SvelteRenderProps["renderChild"] {
111
+ return (
112
+ childTree: WalkedField,
113
+ childValue: unknown,
114
+ childOnChange: (v: unknown) => void,
115
+ pathSuffix?: string
116
+ ): SvelteRenderDescriptor | null => {
117
+ const childPath = joinPath(parentPath, pathSuffix);
118
+ return renderFieldSvelte(
119
+ childTree,
120
+ childValue,
121
+ childOnChange,
122
+ userResolver,
123
+ makeRenderChild(currentDepth + 1, childPath),
124
+ childPath,
125
+ undefined,
126
+ contextWidgets,
127
+ currentDepth + 1,
128
+ Fallback,
129
+ RecursionSentinel
130
+ );
131
+ };
132
+ }
133
+
134
+ const renderChild = $derived(makeRenderChild(0, rootPath));
135
+
136
+ const rootDescriptor = $derived<SvelteRenderDescriptor | null>(
137
+ renderFieldSvelte(
138
+ fieldTree,
139
+ fieldValue,
140
+ handleChange,
141
+ userResolver,
142
+ renderChild,
143
+ rootPath,
144
+ undefined,
145
+ contextWidgets,
146
+ 0,
147
+ Fallback,
148
+ RecursionSentinel
149
+ )
150
+ );
151
+
152
+ function joinPath(parent: string, suffix: string | undefined): string {
153
+ if (suffix === undefined || suffix.length === 0) return parent;
154
+ if (parent.length === 0) return suffix;
155
+ if (suffix.startsWith("[")) return `${parent}${suffix}`;
156
+ return `${parent}.${suffix}`;
157
+ }
158
+
159
+ function normaliseOrThrow(
160
+ schemaInput: unknown,
161
+ refInput: string | undefined,
162
+ ioSide: SchemaIoSide | undefined
163
+ ): NormalisedShape {
164
+ try {
165
+ const opts =
166
+ ioSide !== undefined ? { io: ioSide } : undefined;
167
+ const out = normaliseSchema(schemaInput, refInput, opts);
168
+ return out;
169
+ } catch (err: unknown) {
170
+ if (err instanceof SchemaNormalisationError) throw err;
171
+ throw new SchemaNormalisationError(
172
+ err instanceof Error
173
+ ? err.message
174
+ : "Failed to normalise schema",
175
+ schemaInput,
176
+ "unknown"
177
+ );
178
+ }
179
+ }
180
+
181
+ function resolvePathOrThrow(
182
+ tree: WalkedField,
183
+ p: string,
184
+ schemaInput: unknown
185
+ ): WalkedField {
186
+ const resolved = resolvePath(tree, p);
187
+ if (resolved === undefined) {
188
+ throw new SchemaFieldError(
189
+ `Field not found: ${p}`,
190
+ schemaInput,
191
+ p
192
+ );
193
+ }
194
+ return resolved;
195
+ }
196
+ </script>
197
+
198
+ <script lang="ts" module>
199
+ let fieldInstanceCounter = 0;
200
+ /**
201
+ * Module-scoped counter feeding the default `idPrefix` for every
202
+ * `<SchemaField>` instance. Separate from the `<SchemaComponent>`
203
+ * and `<SchemaView>` counters so the three namespaces never
204
+ * overlap.
205
+ *
206
+ * @returns The next per-instance integer (1, 2, …).
207
+ */
208
+ export function nextFieldInstanceId(): number {
209
+ fieldInstanceCounter += 1;
210
+ return fieldInstanceCounter;
211
+ }
212
+ </script>
213
+
214
+ {#if rootDescriptor !== null}
215
+ <Mount descriptor={rootDescriptor} />
216
+ {/if}
@@ -0,0 +1,46 @@
1
+ <!--
2
+ Provide a theme resolver and scoped widgets to every
3
+ `<SchemaComponent>` and `<SchemaView>` rendered inside the
4
+ subtree. Mirror of `react/SchemaComponent.tsx :: SchemaProvider`
5
+ for Svelte 5.
6
+
7
+ Sets the resolver and widget map via `setContext()`
8
+ (`./contexts.ts :: resolverContext`, `widgetsContext`) so any
9
+ descendant calling `resolverContext.consume()` or
10
+ `widgetsContext.consume()` receives the supplied values.
11
+ `<SchemaComponent>` calls those consume hooks on mount and will
12
+ therefore pick up the provided theme automatically.
13
+ -->
14
+ <script lang="ts">
15
+ import type { Snippet } from "svelte";
16
+ import { resolverContext, widgetsContext } from "./contexts.ts";
17
+ import type { SvelteComponentResolver, SvelteWidgetMap } from "./types.ts";
18
+
19
+ interface Props {
20
+ /** Theme resolver to install for the subtree. */
21
+ resolver: SvelteComponentResolver;
22
+ /** Scoped widgets — override per-instance and global widgets. */
23
+ widgets?: SvelteWidgetMap;
24
+ children: Snippet;
25
+ }
26
+
27
+ const { resolver, widgets, children }: Props = $props();
28
+
29
+ // Svelte's `setContext` is one-shot per component mount; the
30
+ // provide port returns its `children` argument so the call site
31
+ // gets a familiar value-returning shape, but the actual context
32
+ // wiring happens as a side effect. Reactive updates to
33
+ // `resolver` / `widgets` after mount intentionally do not
34
+ // propagate — matches the React adapter's
35
+ // `<UserResolverContext.Provider value={resolver}>` semantics
36
+ // where re-rendering the provider with a new value would update
37
+ // consumers; here we contract the value as mount-fixed and
38
+ // recommend re-mounting the subtree when the theme changes.
39
+
40
+ /* svelte-ignore state_referenced_locally */
41
+ resolverContext.provide(resolver, undefined);
42
+ /* svelte-ignore state_referenced_locally */
43
+ widgetsContext.provide(widgets, undefined);
44
+ </script>
45
+
46
+ {@render children()}
@@ -0,0 +1,244 @@
1
+ <!--
2
+ Read-only Svelte 5 schema renderer — counterpart to
3
+ `<SchemaComponent>` for SSR / hydration / display-only use.
4
+
5
+ Mirror of `react/SchemaView.tsx`. Receives the same prop bag but
6
+ drops `onChange`, `validate`, `onValidationError`, and `onError`
7
+ — the read-only path has no interactive state and no validation
8
+ to dispatch. The theme adapter is passed via the `resolver` prop
9
+ rather than `<SchemaProvider>` so the component remains usable in
10
+ Svelte's SSR pass without depending on context propagation.
11
+
12
+ Internally, the dispatcher chain is identical to
13
+ `<SchemaComponent>` — `renderFieldSvelte` from `./dispatch.ts`
14
+ handles the depth cap, widget overrides, resolver dispatch, and
15
+ recursion sentinel. The only differences are:
16
+
17
+ - `readOnly` is forced to `true` on the merged meta.
18
+ - `onChange` is a noop (no event handlers fire; every renderer
19
+ observes `readOnly === true` and skips wiring DOM handlers).
20
+ - No global widget lookup — Svelte SSR mustn't read
21
+ module-level mutable state.
22
+ -->
23
+ <script lang="ts" generics="T = unknown, Ref extends string | undefined = undefined">
24
+ import { walk } from "../core/walker.ts";
25
+ import type { WalkOptions } from "../core/walkBuilders.ts";
26
+ import {
27
+ normaliseSchema,
28
+ type SchemaIoSide,
29
+ } from "../core/adapter.ts";
30
+ import { SchemaNormalisationError } from "../core/errors.ts";
31
+ import type { SchemaMeta, WalkedField } from "../core/types.ts";
32
+ import { toRecordOrUndefined } from "../core/guards.ts";
33
+ import type {
34
+ DiagnosticsOptions,
35
+ Diagnostic,
36
+ } from "../core/diagnostics.ts";
37
+ import type {
38
+ InferFields,
39
+ InferredValue,
40
+ } from "../core/inferValue.ts";
41
+ import { renderFieldSvelte } from "./dispatch.ts";
42
+ import RecursionSentinel from "./renderers/RecursionSentinel.svelte";
43
+ import Fallback from "./renderers/Fallback.svelte";
44
+ import Mount from "./renderers/Mount.svelte";
45
+ import type {
46
+ SvelteComponentResolver,
47
+ SvelteRenderDescriptor,
48
+ SvelteRenderProps,
49
+ SvelteWidgetMap,
50
+ } from "./types.ts";
51
+
52
+ interface Props {
53
+ schema: T;
54
+ ref?: Ref;
55
+ io?: SchemaIoSide;
56
+ value?: InferredValue<T, Ref, undefined, "output">;
57
+ fields?: InferFields<T, Ref>;
58
+ meta?: SchemaMeta;
59
+ description?: string;
60
+ /** Theme resolver — Svelte SSR has no context fallthrough, pass explicitly. */
61
+ resolver?: SvelteComponentResolver;
62
+ /** Instance-scoped widgets. */
63
+ widgets?: SvelteWidgetMap;
64
+ onDiagnostic?: (diagnostic: Diagnostic) => void;
65
+ strict?: boolean;
66
+ idPrefix?: string;
67
+ }
68
+
69
+ const {
70
+ schema,
71
+ ref,
72
+ io,
73
+ value,
74
+ fields,
75
+ meta: componentMeta,
76
+ description,
77
+ resolver,
78
+ widgets,
79
+ onDiagnostic,
80
+ strict,
81
+ idPrefix,
82
+ }: Props = $props();
83
+
84
+ /**
85
+ * Per-mount fallback prefix — matches `<SchemaComponent>`'s
86
+ * default so a `<SchemaView>` and a sibling `<SchemaComponent>`
87
+ * on the same page can never collide.
88
+ */
89
+ const fallbackPrefix = `sc-svelte-view-${String(nextViewInstanceId())}`;
90
+ const rootPath = $derived(idPrefix ?? fallbackPrefix);
91
+
92
+ const mergedMeta = $derived<SchemaMeta>({
93
+ ...componentMeta,
94
+ readOnly: true,
95
+ ...(description !== undefined ? { description } : {}),
96
+ });
97
+
98
+ const diagnostics: DiagnosticsOptions | undefined = $derived(
99
+ onDiagnostic !== undefined || strict === true
100
+ ? {
101
+ ...(onDiagnostic !== undefined
102
+ ? { diagnostics: onDiagnostic }
103
+ : {}),
104
+ ...(strict !== undefined ? { strict } : {}),
105
+ }
106
+ : undefined
107
+ );
108
+
109
+ interface NormalisedShape {
110
+ jsonSchema: Record<string, unknown>;
111
+ rootMeta: SchemaMeta | undefined;
112
+ rootDocument: Record<string, unknown>;
113
+ }
114
+
115
+ const normalisedResult = $derived<NormalisedShape>(
116
+ normaliseOrThrow(schema, ref, io, diagnostics)
117
+ );
118
+
119
+ const fieldsRecord = $derived(toRecordOrUndefined(fields));
120
+
121
+ const walkOptions = $derived<WalkOptions>({
122
+ componentMeta: mergedMeta,
123
+ ...(normalisedResult.rootMeta !== undefined
124
+ ? { rootMeta: normalisedResult.rootMeta }
125
+ : {}),
126
+ ...(fieldsRecord !== undefined
127
+ ? { fieldOverrides: fieldsRecord }
128
+ : {}),
129
+ rootDocument: normalisedResult.rootDocument,
130
+ ...(diagnostics !== undefined ? { diagnostics } : {}),
131
+ });
132
+
133
+ const tree = $derived<WalkedField>(
134
+ walk(normalisedResult.jsonSchema, walkOptions)
135
+ );
136
+
137
+ function readOnlyOnChange(_v: unknown): void {
138
+ /* intentional no-op — SchemaView is read-only. */
139
+ }
140
+
141
+ function makeRenderChild(
142
+ currentDepth: number,
143
+ parentPath: string
144
+ ): SvelteRenderProps["renderChild"] {
145
+ return (
146
+ childTree: WalkedField,
147
+ childValue: unknown,
148
+ _childOnChange: (v: unknown) => void,
149
+ pathSuffix?: string
150
+ ): SvelteRenderDescriptor | null => {
151
+ const childPath = joinPath(parentPath, pathSuffix);
152
+ return renderFieldSvelte(
153
+ childTree,
154
+ childValue,
155
+ readOnlyOnChange,
156
+ resolver,
157
+ makeRenderChild(currentDepth + 1, childPath),
158
+ childPath,
159
+ widgets,
160
+ undefined,
161
+ currentDepth + 1,
162
+ Fallback,
163
+ RecursionSentinel
164
+ );
165
+ };
166
+ }
167
+
168
+ const renderChild = $derived(makeRenderChild(0, rootPath));
169
+
170
+ const rootDescriptor = $derived<SvelteRenderDescriptor | null>(
171
+ renderFieldSvelte(
172
+ tree,
173
+ value ?? tree.defaultValue,
174
+ readOnlyOnChange,
175
+ resolver,
176
+ renderChild,
177
+ rootPath,
178
+ widgets,
179
+ undefined,
180
+ 0,
181
+ Fallback,
182
+ RecursionSentinel
183
+ )
184
+ );
185
+
186
+ /**
187
+ * Path-suffix join, mirror of `SchemaComponent.svelte :: joinPath`
188
+ * and `react/SchemaComponent.tsx :: joinPath`.
189
+ */
190
+ function joinPath(parent: string, suffix: string | undefined): string {
191
+ if (suffix === undefined || suffix.length === 0) return parent;
192
+ if (parent.length === 0) return suffix;
193
+ if (suffix.startsWith("[")) return `${parent}${suffix}`;
194
+ return `${parent}.${suffix}`;
195
+ }
196
+
197
+ function normaliseOrThrow(
198
+ schemaInput: unknown,
199
+ refInput: string | undefined,
200
+ ioSide: SchemaIoSide | undefined,
201
+ diags: DiagnosticsOptions | undefined
202
+ ): NormalisedShape {
203
+ try {
204
+ const opts =
205
+ diags !== undefined || ioSide !== undefined
206
+ ? {
207
+ ...(diags !== undefined ? { diagnostics: diags } : {}),
208
+ ...(ioSide !== undefined ? { io: ioSide } : {}),
209
+ }
210
+ : undefined;
211
+ const out = normaliseSchema(schemaInput, refInput, opts);
212
+ return out;
213
+ } catch (err: unknown) {
214
+ if (err instanceof SchemaNormalisationError) throw err;
215
+ throw new SchemaNormalisationError(
216
+ err instanceof Error
217
+ ? err.message
218
+ : "Failed to normalise schema",
219
+ schemaInput,
220
+ "unknown"
221
+ );
222
+ }
223
+ }
224
+ </script>
225
+
226
+ <script lang="ts" module>
227
+ let viewInstanceCounter = 0;
228
+ /**
229
+ * Module-scoped counter feeding the default `idPrefix` for every
230
+ * `<SchemaView>` instance. Separate from the `<SchemaComponent>`
231
+ * counter so the two namespaces never overlap; ids stay unique
232
+ * across all schema-driven Svelte components on a page.
233
+ *
234
+ * @returns The next per-instance integer (1, 2, …).
235
+ */
236
+ export function nextViewInstanceId(): number {
237
+ viewInstanceCounter += 1;
238
+ return viewInstanceCounter;
239
+ }
240
+ </script>
241
+
242
+ {#if rootDescriptor !== null}
243
+ <Mount descriptor={rootDescriptor} />
244
+ {/if}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared ARIA attribute helpers for Svelte 5 renderers.
3
+ *
4
+ * Mirror of `react/a11y.ts` returning the same shapes — plain
5
+ * `Record<string, string>` spreadable attribute bags and a
6
+ * {@link HintInfo} descriptor for the constraint-hint `<small>`
7
+ * element — so theme adapters compose around the same accessibility
8
+ * primitives the headless renderer emits.
9
+ *
10
+ * Keep this module aligned with `react/a11y.ts` and `html/a11y.ts` —
11
+ * all renderers must emit the same accessibility metadata for the
12
+ * same schema field. The constraint-hint text builder
13
+ * (`core/constraintHint.ts`) is the single source of truth for hint
14
+ * copy; this module wires it into Svelte-flavoured attribute bags
15
+ * and hint descriptors.
16
+ */
17
+
18
+ import type { AllConstraints } from "../core/renderer.ts";
19
+ import type { WalkedField } from "../core/types.ts";
20
+ import { hintIdFor } from "../core/idPath.ts";
21
+ import { constraintHint as coreConstraintHint } from "../core/constraintHint.ts";
22
+
23
+ /**
24
+ * Build the ARIA attribute bundle for a Svelte renderer.
25
+ *
26
+ * - `aria-required="true"` whenever the field is non-optional.
27
+ * - `aria-describedby=<hint-id>` whenever a constraint hint applies.
28
+ * - `aria-label=<description>` when a non-empty description is
29
+ * supplied via the `description` argument.
30
+ *
31
+ * Returns a plain `Record<string, string>` so callers can spread the
32
+ * result into any HTML element type without per-element TypeScript
33
+ * widening. Svelte's `{...attrs}` spread accepts the record directly.
34
+ */
35
+ export function buildAriaAttrs(
36
+ tree: WalkedField,
37
+ description?: unknown,
38
+ inputId?: string,
39
+ constraints?: AllConstraints
40
+ ): Record<string, string> {
41
+ const attrs: Record<string, string> = {};
42
+ if (tree.isOptional === false) {
43
+ attrs["aria-required"] = "true";
44
+ }
45
+ if (
46
+ inputId !== undefined &&
47
+ constraints !== undefined &&
48
+ coreConstraintHint(constraints) !== undefined
49
+ ) {
50
+ attrs["aria-describedby"] = hintIdFor(inputId);
51
+ }
52
+ if (typeof description === "string" && description.length > 0) {
53
+ attrs["aria-label"] = description;
54
+ }
55
+ return attrs;
56
+ }
57
+
58
+ /**
59
+ * Description for a constraint hint emitted alongside an input.
60
+ *
61
+ * Returned by {@link buildHintInfo} when the field carries one or
62
+ * more constraint keywords worth announcing (min / max length,
63
+ * pattern without format, item count, …). Theme adapters render this
64
+ * as a `<small>` wired to the input via `aria-describedby`.
65
+ */
66
+ export interface HintInfo {
67
+ /** DOM id matching {@link hintIdFor}(inputId) on the host input. */
68
+ readonly id: string;
69
+ /** Human-readable hint text. */
70
+ readonly hint: string;
71
+ /** Same value as {@link id}, exposed under the React-aligned name. */
72
+ readonly ariaDescribedBy: string;
73
+ }
74
+
75
+ /**
76
+ * Build {@link HintInfo} for a field at `inputId` given its declared
77
+ * constraints. Returns `undefined` when no constraint message would
78
+ * be produced — the Svelte renderers then skip emitting the hint
79
+ * element entirely so consumers don't see an empty `<small>`.
80
+ */
81
+ export function buildHintInfo(
82
+ inputId: string,
83
+ constraints: AllConstraints
84
+ ): HintInfo | undefined {
85
+ const hint = coreConstraintHint(constraints);
86
+ if (hint === undefined) return undefined;
87
+ const id = hintIdFor(inputId);
88
+ return { id, hint, ariaDescribedBy: id };
89
+ }
90
+
91
+ /**
92
+ * Narrow `meta.description` (typed `unknown`) to a string value safe
93
+ * to pass into an `aria-label` attribute. Returns `undefined` for
94
+ * non-string or empty-string descriptions so Svelte drops the
95
+ * attribute rather than stringifying e.g. `{}` to
96
+ * `"[object Object]"`.
97
+ */
98
+ export function ariaLabel(description: unknown): string | undefined {
99
+ if (typeof description !== "string") return undefined;
100
+ if (description.length === 0) return undefined;
101
+ return description;
102
+ }
103
+
104
+ /**
105
+ * True when the supplied field is non-optional and therefore
106
+ * deserves a visual required indicator alongside its label. Exposed
107
+ * as a predicate (rather than a built element) so theme adapters can
108
+ * choose any presentation.
109
+ */
110
+ export function isFieldRequired(tree: WalkedField): boolean {
111
+ return tree.isOptional === false;
112
+ }