schema-components 2.0.2 → 2.1.1

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 (197) hide show
  1. package/README.md +133 -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/constraintHint.d.mts +1 -1
  8. package/dist/core/constraints.d.mts +2 -2
  9. package/dist/core/contexts.d.mts +71 -0
  10. package/dist/core/contexts.mjs +1 -0
  11. package/dist/core/diagnostics.d.mts +1 -1
  12. package/dist/core/errors.d.mts +1 -1
  13. package/dist/core/fieldOrder.d.mts +1 -1
  14. package/dist/{react → core}/fieldPath.d.mts +2 -2
  15. package/dist/{react → core}/fieldPath.mjs +3 -3
  16. package/dist/core/formats.d.mts +1 -1
  17. package/dist/core/inferValue.d.mts +1 -1
  18. package/dist/core/limits.d.mts +1 -1
  19. package/dist/core/merge.d.mts +1 -1
  20. package/dist/core/normalise.d.mts +2 -2
  21. package/dist/core/ref.d.mts +1 -1
  22. package/dist/core/renderField.d.mts +147 -0
  23. package/dist/core/renderField.mjs +115 -0
  24. package/dist/core/renderer.d.mts +2 -199
  25. package/dist/core/swagger2.d.mts +1 -1
  26. package/dist/core/typeInference.d.mts +1 -982
  27. package/dist/core/types.d.mts +1 -1
  28. package/dist/core/unionMatch.d.mts +1 -1
  29. package/dist/core/version.d.mts +1 -1
  30. package/dist/core/walkBuilders.d.mts +3 -3
  31. package/dist/core/walker.d.mts +1 -1
  32. package/dist/{errors-Dki7tji4.d.mts → errors-DbaI04x2.d.mts} +1 -1
  33. package/dist/html/a11y.d.mts +2 -2
  34. package/dist/html/renderToHtml.d.mts +5 -5
  35. package/dist/html/renderToHtml.mjs +33 -18
  36. package/dist/html/renderToHtmlStream.d.mts +5 -5
  37. package/dist/html/renderers.d.mts +1 -1
  38. package/dist/html/streamRenderers.d.mts +15 -6
  39. package/dist/html/streamRenderers.mjs +56 -10
  40. package/dist/{inferValue-Ce-PviSD.d.mts → inferValue-eAnh50EM.d.mts} +3 -3
  41. package/dist/lit/SchemaComponent.d.mts +125 -0
  42. package/dist/lit/SchemaComponent.mjs +2 -0
  43. package/dist/lit/SchemaField.d.mts +65 -0
  44. package/dist/lit/SchemaField.mjs +2 -0
  45. package/dist/lit/SchemaView.d.mts +14 -0
  46. package/dist/lit/SchemaView.mjs +2 -0
  47. package/dist/lit/constructorTypes.d.mts +2 -0
  48. package/dist/lit/constructorTypes.mjs +1 -0
  49. package/dist/lit/contexts.d.mts +78 -0
  50. package/dist/lit/contexts.mjs +238 -0
  51. package/dist/lit/defaultResolver.d.mts +33 -0
  52. package/dist/lit/defaultResolver.mjs +2 -0
  53. package/dist/lit/registry.d.mts +66 -0
  54. package/dist/lit/registry.mjs +2 -0
  55. package/dist/lit/renderers/baseElement.d.mts +131 -0
  56. package/dist/lit/renderers/baseElement.mjs +109 -0
  57. package/dist/lit/renderers/recordHelpers.d.mts +25 -0
  58. package/dist/lit/renderers/recordHelpers.mjs +55 -0
  59. package/dist/lit/renderers/scArray.d.mts +14 -0
  60. package/dist/lit/renderers/scArray.mjs +86 -0
  61. package/dist/lit/renderers/scBoolean.d.mts +15 -0
  62. package/dist/lit/renderers/scBoolean.mjs +47 -0
  63. package/dist/lit/renderers/scConditional.d.mts +23 -0
  64. package/dist/lit/renderers/scConditional.mjs +65 -0
  65. package/dist/lit/renderers/scDiscriminated.d.mts +23 -0
  66. package/dist/lit/renderers/scDiscriminated.mjs +138 -0
  67. package/dist/lit/renderers/scEnum.d.mts +16 -0
  68. package/dist/lit/renderers/scEnum.mjs +66 -0
  69. package/dist/lit/renderers/scFile.d.mts +15 -0
  70. package/dist/lit/renderers/scFile.mjs +53 -0
  71. package/dist/lit/renderers/scLiteralNullNever.d.mts +30 -0
  72. package/dist/lit/renderers/scLiteralNullNever.mjs +57 -0
  73. package/dist/lit/renderers/scNumber.d.mts +15 -0
  74. package/dist/lit/renderers/scNumber.mjs +64 -0
  75. package/dist/lit/renderers/scObject.d.mts +14 -0
  76. package/dist/lit/renderers/scObject.mjs +57 -0
  77. package/dist/lit/renderers/scRecord.d.mts +14 -0
  78. package/dist/lit/renderers/scRecord.mjs +112 -0
  79. package/dist/lit/renderers/scString.d.mts +19 -0
  80. package/dist/lit/renderers/scString.mjs +165 -0
  81. package/dist/lit/renderers/scTuple.d.mts +14 -0
  82. package/dist/lit/renderers/scTuple.mjs +58 -0
  83. package/dist/lit/renderers/scUnion.d.mts +14 -0
  84. package/dist/lit/renderers/scUnion.mjs +44 -0
  85. package/dist/lit/renderers/scUnknown.d.mts +15 -0
  86. package/dist/lit/renderers/scUnknown.mjs +45 -0
  87. package/dist/lit/ssr.d.mts +37 -0
  88. package/dist/lit/ssr.mjs +9565 -0
  89. package/dist/lit/types.d.mts +2 -0
  90. package/dist/lit/types.mjs +1 -0
  91. package/dist/lit/widget.d.mts +71 -0
  92. package/dist/lit/widget.mjs +87 -0
  93. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  94. package/dist/openapi/ApiLinks.d.mts +1 -1
  95. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  96. package/dist/openapi/ApiSecurity.d.mts +1 -1
  97. package/dist/openapi/components.d.mts +4 -4
  98. package/dist/openapi/parser.d.mts +2 -2
  99. package/dist/openapi/resolve.d.mts +1 -1
  100. package/dist/preact/SchemaComponent.d.mts +3 -0
  101. package/dist/preact/SchemaComponent.mjs +26 -0
  102. package/dist/preact/SchemaErrorBoundary.d.mts +2 -0
  103. package/dist/preact/SchemaErrorBoundary.mjs +20 -0
  104. package/dist/preact/SchemaView.d.mts +2 -0
  105. package/dist/preact/SchemaView.mjs +22 -0
  106. package/dist/preact/headless.d.mts +2 -0
  107. package/dist/preact/headless.mjs +18 -0
  108. package/dist/react/SchemaComponent.d.mts +3 -270
  109. package/dist/react/SchemaComponent.mjs +41 -32
  110. package/dist/react/SchemaView.d.mts +6 -6
  111. package/dist/react/SchemaView.mjs +32 -29
  112. package/dist/react/a11y.d.mts +2 -2
  113. package/dist/react/fieldShell.d.mts +1 -1
  114. package/dist/react/headless.d.mts +1 -1
  115. package/dist/react/headlessRenderers.d.mts +2 -2
  116. package/dist/{ref-DdsbekXX.d.mts → ref-DWrQG1Er.d.mts} +1 -1
  117. package/dist/renderer-ab9E52Bp.d.mts +245 -0
  118. package/dist/solid/SchemaComponent.d.mts +136 -0
  119. package/dist/solid/SchemaComponent.mjs +391 -0
  120. package/dist/solid/SchemaErrorBoundary.d.mts +38 -0
  121. package/dist/solid/SchemaErrorBoundary.mjs +57 -0
  122. package/dist/solid/SchemaField.d.mts +40 -0
  123. package/dist/solid/SchemaField.mjs +113 -0
  124. package/dist/solid/SchemaView.d.mts +54 -0
  125. package/dist/solid/SchemaView.mjs +168 -0
  126. package/dist/solid/a11y.d.mts +70 -0
  127. package/dist/solid/a11y.mjs +71 -0
  128. package/dist/solid/contexts.d.mts +37 -0
  129. package/dist/solid/contexts.mjs +66 -0
  130. package/dist/solid/headless.d.mts +10 -0
  131. package/dist/solid/headless.mjs +27 -0
  132. package/dist/solid/renderers.d.mts +79 -0
  133. package/dist/solid/renderers.mjs +840 -0
  134. package/dist/solid/types.d.mts +90 -0
  135. package/dist/solid/types.mjs +1 -0
  136. package/dist/solid/widget.d.mts +29 -0
  137. package/dist/solid/widget.mjs +35 -0
  138. package/dist/themes/mantine.d.mts +1 -1
  139. package/dist/themes/mui.d.mts +1 -1
  140. package/dist/themes/radix.d.mts +1 -1
  141. package/dist/themes/shadcn.d.mts +1 -1
  142. package/dist/typeInference-Y8tNEQJk.d.mts +983 -0
  143. package/dist/types-BCy7K3nk.d.mts +125 -0
  144. package/package.json +73 -1
  145. package/src/svelte/SchemaComponent.svelte +427 -0
  146. package/src/svelte/SchemaErrorBoundary.svelte +66 -0
  147. package/src/svelte/SchemaField.svelte +216 -0
  148. package/src/svelte/SchemaProvider.svelte +46 -0
  149. package/src/svelte/SchemaView.svelte +244 -0
  150. package/src/svelte/a11y.ts +112 -0
  151. package/src/svelte/contexts.ts +79 -0
  152. package/src/svelte/dispatch.ts +267 -0
  153. package/src/svelte/headless.ts +73 -0
  154. package/src/svelte/headlessFns.ts +124 -0
  155. package/src/svelte/renderers/Array.svelte +98 -0
  156. package/src/svelte/renderers/Boolean.svelte +43 -0
  157. package/src/svelte/renderers/Conditional.svelte +67 -0
  158. package/src/svelte/renderers/DiscriminatedUnion.svelte +197 -0
  159. package/src/svelte/renderers/Enum.svelte +53 -0
  160. package/src/svelte/renderers/Fallback.svelte +24 -0
  161. package/src/svelte/renderers/File.svelte +46 -0
  162. package/src/svelte/renderers/Literal.svelte +29 -0
  163. package/src/svelte/renderers/Mount.svelte +24 -0
  164. package/src/svelte/renderers/Negation.svelte +35 -0
  165. package/src/svelte/renderers/Never.svelte +24 -0
  166. package/src/svelte/renderers/Null.svelte +19 -0
  167. package/src/svelte/renderers/Number.svelte +68 -0
  168. package/src/svelte/renderers/Object.svelte +74 -0
  169. package/src/svelte/renderers/Record.svelte +134 -0
  170. package/src/svelte/renderers/RecursionSentinel.svelte +27 -0
  171. package/src/svelte/renderers/String.svelte +152 -0
  172. package/src/svelte/renderers/Tuple.svelte +84 -0
  173. package/src/svelte/renderers/Union.svelte +49 -0
  174. package/src/svelte/renderers/Unknown.svelte +42 -0
  175. package/src/svelte/svelte-modules.d.ts +25 -0
  176. package/src/svelte/types.ts +238 -0
  177. package/src/svelte/widget.ts +62 -0
  178. package/src/vue/SchemaComponent.vue +274 -0
  179. package/src/vue/SchemaErrorBoundary.vue +60 -0
  180. package/src/vue/SchemaField.vue +178 -0
  181. package/src/vue/SchemaProvider.vue +39 -0
  182. package/src/vue/SchemaView.vue +198 -0
  183. package/src/vue/VNodeHost.ts +32 -0
  184. package/src/vue/contexts.ts +116 -0
  185. package/src/vue/eventTargets.ts +35 -0
  186. package/src/vue/headless.ts +61 -0
  187. package/src/vue/idPrefix.ts +79 -0
  188. package/src/vue/renderField.ts +182 -0
  189. package/src/vue/renderers.ts +1297 -0
  190. package/src/vue/resolver.ts +45 -0
  191. package/src/vue/types.ts +140 -0
  192. package/src/vue/vue-shim.d.ts +25 -0
  193. package/src/vue/widget.ts +51 -0
  194. /package/dist/{diagnostics-BTrm3O6J.d.mts → diagnostics-mftUZI7c.d.mts} +0 -0
  195. /package/dist/{limits-x4OiyJxh.d.mts → limits-Vv9hUbI_.d.mts} +0 -0
  196. /package/dist/{types-BrYbjC7_.d.mts → types-BBQaEPfE.d.mts} +0 -0
  197. /package/dist/{version-DL8U5RuA.d.mts → version-BEBx10ND.d.mts} +0 -0
@@ -0,0 +1,67 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for conditional fields — JSON Schema
3
+ `if` / `then` / `else`. Mirror of
4
+ `react/headlessRenderers.tsx :: renderConditional`.
5
+
6
+ Conditional schemas describe constraints rather than a single
7
+ value shape, so the renderer surfaces each clause as a labelled
8
+ section inside a `<fieldset>`. Mirrors the HTML renderer's
9
+ annotation approach and gives a predictable structure for theme
10
+ adapters that want to override it.
11
+ -->
12
+ <script lang="ts">
13
+ import type { SvelteRenderProps } from "../types.ts";
14
+ import { SC_CLASSES } from "../../core/cssClasses.ts";
15
+ import Mount from "./Mount.svelte";
16
+
17
+ const props = $props<SvelteRenderProps>();
18
+
19
+ const ifClause = $derived(
20
+ props.tree.type === "conditional" ? props.tree.ifClause : undefined
21
+ );
22
+ const thenClause = $derived(
23
+ props.tree.type === "conditional" ? props.tree.thenClause : undefined
24
+ );
25
+ const elseClause = $derived(
26
+ props.tree.type === "conditional" ? props.tree.elseClause : undefined
27
+ );
28
+
29
+ const ifChild = $derived(
30
+ ifClause !== undefined
31
+ ? props.renderChild(ifClause, props.value, props.onChange)
32
+ : null
33
+ );
34
+ const thenChild = $derived(
35
+ thenClause !== undefined
36
+ ? props.renderChild(thenClause, props.value, props.onChange)
37
+ : null
38
+ );
39
+ const elseChild = $derived(
40
+ elseClause !== undefined
41
+ ? props.renderChild(elseClause, props.value, props.onChange)
42
+ : null
43
+ );
44
+ </script>
45
+
46
+ {#if ifClause !== undefined}
47
+ <fieldset class={SC_CLASSES.conditional}>
48
+ <div class={SC_CLASSES.conditionalIf}>
49
+ <strong>if:</strong>
50
+ {#if ifChild !== null}
51
+ <Mount descriptor={ifChild} />
52
+ {/if}
53
+ </div>
54
+ {#if thenClause !== undefined && thenChild !== null}
55
+ <div class={SC_CLASSES.conditionalThen}>
56
+ <strong>then:</strong>
57
+ <Mount descriptor={thenChild} />
58
+ </div>
59
+ {/if}
60
+ {#if elseClause !== undefined && elseChild !== null}
61
+ <div class={SC_CLASSES.conditionalElse}>
62
+ <strong>else:</strong>
63
+ <Mount descriptor={elseChild} />
64
+ </div>
65
+ {/if}
66
+ </fieldset>
67
+ {/if}
@@ -0,0 +1,197 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `DiscriminatedUnionField` —
3
+ tabbed UI driven by the discriminator. Mirror of
4
+ `react/headlessRenderers.tsx :: renderDiscriminatedUnion` plus
5
+ the embedded `DiscriminatedUnionTabs` component.
6
+
7
+ Implements the WAI-ARIA "Tabs with Automatic Activation" pattern:
8
+ ArrowRight / ArrowLeft move between tabs (wrapping at the
9
+ extremes), Home / End jump to the first / last tab, every tab
10
+ carries explicit `aria-selected` (NVDA / JAWS browse-mode read
11
+ selection state only when the attribute is present on every tab),
12
+ `aria-controls` references the shared tab-panel id, and a roving
13
+ `tabindex` (`0` on the active tab, `-1` elsewhere) keeps keyboard
14
+ focus inside the tablist.
15
+
16
+ Focus management state machine — mirrors the React equivalent:
17
+
18
+ 1. Component renders with `activeIndex` derived from the value.
19
+ 2. User presses an arrow / Home / End key → handler computes
20
+ the new index, sets the `pendingFocus` flag, and calls
21
+ `props.onChange({ [discKey]: newLabel })`.
22
+ 3. Parent re-renders with the new value; `activeIndex` updates.
23
+ 4. `$effect` observes the `activeIndex` change AND
24
+ `pendingFocus`; if both are set, focus the new tab and clear
25
+ the flag.
26
+
27
+ Clicks already move focus (the browser's default), so the
28
+ `pendingFocus` flag is only ever set inside the keyboard handler.
29
+ The flag is held in `$state` rather than a plain variable so the
30
+ effect re-runs reliably when it transitions.
31
+
32
+ `$bindable` is intentionally not used — `value`/`onChange` is the
33
+ canonical contract across React, HTML, and Svelte. Consumers who
34
+ want `bind:value` ergonomics use `<SchemaComponent {schema}
35
+ bind:value />` at the call site; Svelte translates that into an
36
+ `onChange` that mutates the bound rune-backed reference, so this
37
+ component never sees the binding.
38
+ -->
39
+ <script lang="ts">
40
+ import type { SvelteRenderProps } from "../types.ts";
41
+ import { panelIdFor, tabIdFor } from "../../core/idPath.ts";
42
+ import { EM_DASH } from "../../core/cssClasses.ts";
43
+ import { isObject } from "../../core/guards.ts";
44
+ import { resolveDiscriminatedActive } from "../../core/unionMatch.ts";
45
+ import {
46
+ discriminatedUnionValueForTab,
47
+ wrapTabIndex,
48
+ } from "../headlessFns.ts";
49
+ import Mount from "./Mount.svelte";
50
+
51
+ const props = $props<SvelteRenderProps>();
52
+
53
+ const tabRefs: (HTMLButtonElement | null)[] = $state([]);
54
+ let pendingFocus = $state(false);
55
+
56
+ const valueObject = $derived(
57
+ isObject(props.value) ? props.value : undefined
58
+ );
59
+
60
+ const resolved = $derived(
61
+ props.tree.type === "discriminatedUnion"
62
+ ? resolveDiscriminatedActive(
63
+ props.tree.options,
64
+ props.tree.discriminator,
65
+ valueObject
66
+ )
67
+ : { optionLabels: [], activeIndex: 0, activeOption: undefined }
68
+ );
69
+
70
+ const options = $derived(
71
+ props.tree.type === "discriminatedUnion" ? props.tree.options : []
72
+ );
73
+
74
+ const discKey = $derived(
75
+ props.tree.type === "discriminatedUnion" ? props.tree.discriminator : ""
76
+ );
77
+
78
+ const panelId = $derived(panelIdFor(props.path));
79
+
80
+ function handleTabChange(newIndex: number): void {
81
+ const next = discriminatedUnionValueForTab(
82
+ resolved.optionLabels,
83
+ discKey,
84
+ newIndex
85
+ );
86
+ if (next === undefined) return;
87
+ props.onChange(next);
88
+ }
89
+
90
+ function handleKeyDown(e: KeyboardEvent): void {
91
+ let target: number | undefined;
92
+ if (e.key === "ArrowRight")
93
+ target = wrapTabIndex(resolved.activeIndex + 1, options.length);
94
+ else if (e.key === "ArrowLeft")
95
+ target = wrapTabIndex(resolved.activeIndex - 1, options.length);
96
+ else if (e.key === "Home") target = 0;
97
+ else if (e.key === "End") target = options.length - 1;
98
+ if (target === undefined) return;
99
+ e.preventDefault();
100
+ if (target === resolved.activeIndex) return;
101
+ pendingFocus = true;
102
+ handleTabChange(target);
103
+ }
104
+
105
+ /**
106
+ * After a keyboard-driven activeIndex change, move focus to the
107
+ * newly active tab. Skipped on initial mount and after clicks
108
+ * because `pendingFocus` is only set inside `handleKeyDown`.
109
+ */
110
+ $effect(() => {
111
+ // Re-run when `activeIndex` changes.
112
+ const index = resolved.activeIndex;
113
+ if (!pendingFocus) return;
114
+ pendingFocus = false;
115
+ const tab = tabRefs[index];
116
+ if (tab !== null && tab !== undefined) tab.focus();
117
+ });
118
+ </script>
119
+
120
+ {#if props.tree.type !== "discriminatedUnion" || options.length === 0}
121
+ {#if props.value === undefined || props.value === null}
122
+ <span>{EM_DASH}</span>
123
+ {:else}
124
+ <span>{JSON.stringify(props.value)}</span>
125
+ {/if}
126
+ {:else if props.readOnly}
127
+ {#if resolved.activeOption !== undefined}
128
+ {@const child = props.renderChild(
129
+ resolved.activeOption,
130
+ props.value,
131
+ props.onChange
132
+ )}
133
+ {#if child !== null}
134
+ <Mount descriptor={child} />
135
+ {:else}
136
+ <span>{EM_DASH}</span>
137
+ {/if}
138
+ {:else}
139
+ <span>{EM_DASH}</span>
140
+ {/if}
141
+ {:else}
142
+ <div>
143
+ <!--
144
+ `role="tablist"` requires a tabindex per WAI-ARIA. The
145
+ tablist itself never receives focus directly — the roving
146
+ tabindex on the individual tabs handles keyboard entry —
147
+ so set `tabindex=-1` to satisfy the rule without
148
+ introducing a redundant focus stop.
149
+ -->
150
+ <div
151
+ role="tablist"
152
+ tabindex={-1}
153
+ aria-label="Select variant"
154
+ aria-orientation="horizontal"
155
+ style="display: flex; gap: 0.25rem; margin-bottom: 0.5rem;"
156
+ onkeydown={handleKeyDown}
157
+ >
158
+ {#each options as _opt, i (i)}
159
+ <button
160
+ bind:this={tabRefs[i]}
161
+ type="button"
162
+ role="tab"
163
+ id={tabIdFor(props.path, i)}
164
+ aria-selected={i === resolved.activeIndex ? "true" : "false"}
165
+ aria-controls={panelId}
166
+ tabindex={i === resolved.activeIndex ? 0 : -1}
167
+ onclick={() => handleTabChange(i)}
168
+ style="padding: 0.25rem 0.75rem; border: 1px solid {i ===
169
+ resolved.activeIndex
170
+ ? '#3b82f6'
171
+ : '#d1d5db'}; border-radius: 0.25rem; background: {i ===
172
+ resolved.activeIndex
173
+ ? '#eff6ff'
174
+ : 'transparent'}; cursor: pointer; font-size: 0.875rem;"
175
+ >
176
+ {resolved.optionLabels[i]}
177
+ </button>
178
+ {/each}
179
+ </div>
180
+ <div
181
+ role="tabpanel"
182
+ id={panelId}
183
+ aria-labelledby={tabIdFor(props.path, resolved.activeIndex)}
184
+ >
185
+ {#if resolved.activeOption !== undefined}
186
+ {@const child = props.renderChild(
187
+ resolved.activeOption,
188
+ props.value,
189
+ props.onChange
190
+ )}
191
+ {#if child !== null}
192
+ <Mount descriptor={child} />
193
+ {/if}
194
+ {/if}
195
+ </div>
196
+ </div>
197
+ {/if}
@@ -0,0 +1,53 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `EnumField` — plain `<select>`
3
+ mirror of `react/headlessRenderers.tsx :: renderEnum`. Lists
4
+ every enumerated value through `displayJsonValue` and wires the
5
+ constraint hint via `aria-describedby` when present.
6
+
7
+ Read-only mode emits the current value through a `<span>`, falling
8
+ back to an em-dash for empty / undefined values.
9
+ -->
10
+ <script lang="ts">
11
+ import type { SvelteRenderProps } from "../types.ts";
12
+ import { fieldDomId } from "../../core/idPath.ts";
13
+ import { EM_DASH, ELLIPSIS } from "../../core/cssClasses.ts";
14
+ import { displayJsonValue } from "../../core/walkBuilders.ts";
15
+ import { buildAriaAttrs, buildHintInfo } from "../a11y.ts";
16
+
17
+ const props = $props<SvelteRenderProps>();
18
+
19
+ const id = $derived(fieldDomId(props.path));
20
+ const enumValue = $derived(
21
+ typeof props.value === "string" ? props.value : ""
22
+ );
23
+ </script>
24
+
25
+ {#if props.readOnly}
26
+ <span {id}>{enumValue.length > 0 ? enumValue : EM_DASH}</span>
27
+ {:else}
28
+ {@const ariaAttrs = buildAriaAttrs(props.tree)}
29
+ {@const hintInfo = buildHintInfo(id, props.constraints)}
30
+ {@const enumValues =
31
+ props.tree.type === "enum" ? props.tree.enumValues : []}
32
+
33
+ <select
34
+ {id}
35
+ value={props.writeOnly ? "" : enumValue}
36
+ onchange={(e) => {
37
+ const target = e.currentTarget;
38
+ if (target instanceof HTMLSelectElement) {
39
+ props.onChange(target.value);
40
+ }
41
+ }}
42
+ aria-describedby={hintInfo?.ariaDescribedBy}
43
+ {...ariaAttrs}
44
+ >
45
+ <option value="">Select{ELLIPSIS}</option>
46
+ {#each enumValues as v (displayJsonValue(v))}
47
+ <option value={displayJsonValue(v)}>{displayJsonValue(v)}</option>
48
+ {/each}
49
+ </select>
50
+ {#if hintInfo !== undefined}
51
+ <small id={hintInfo.id} class="sc-hint">{hintInfo.hint}</small>
52
+ {/if}
53
+ {/if}
@@ -0,0 +1,24 @@
1
+ <!--
2
+ Fallback Svelte renderer emitted by the dispatcher when no widget
3
+ or resolver render function produced output. Mirror of the React
4
+ fallback `<span>—</span>` / `<span>{JSON.stringify(value)}</span>`
5
+ in `react/SchemaComponent.tsx :: renderField`.
6
+
7
+ Renders an em-dash for undefined / null values and a stringified
8
+ JSON projection otherwise so unhandled positions remain visible
9
+ without a runtime crash.
10
+ -->
11
+ <script lang="ts">
12
+ import type { SvelteRenderProps } from "../types.ts";
13
+ import { EM_DASH } from "../../core/cssClasses.ts";
14
+
15
+ const props = $props<SvelteRenderProps>();
16
+ </script>
17
+
18
+ {#if props.value === undefined || props.value === null}
19
+ <span>{EM_DASH}</span>
20
+ {:else if typeof props.value === "string"}
21
+ <span>{props.value}</span>
22
+ {:else}
23
+ <span>{JSON.stringify(props.value)}</span>
24
+ {/if}
@@ -0,0 +1,46 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `FileField` — plain
3
+ `<input type="file">`. Mirror of
4
+ `react/headlessRenderers.tsx :: renderFile`.
5
+
6
+ Wires the first MIME type / array of MIME types from the
7
+ constraint bag into the `accept` attribute. Read-only mode emits
8
+ a placeholder span — file inputs cannot meaningfully be rendered
9
+ read-only.
10
+ -->
11
+ <script lang="ts">
12
+ import type { SvelteRenderProps } from "../types.ts";
13
+ import { fieldDomId } from "../../core/idPath.ts";
14
+ import { buildAriaAttrs, buildHintInfo } from "../a11y.ts";
15
+
16
+ const props = $props<SvelteRenderProps>();
17
+
18
+ const id = $derived(fieldDomId(props.path));
19
+ const accept = $derived(props.constraints.mimeTypes?.join(","));
20
+ </script>
21
+
22
+ {#if props.readOnly}
23
+ <span {id}>{"File field"}</span>
24
+ {:else}
25
+ {@const ariaAttrs = buildAriaAttrs(props.tree, props.meta.description)}
26
+ {@const hintInfo = buildHintInfo(id, props.constraints)}
27
+ <input
28
+ {id}
29
+ type="file"
30
+ {accept}
31
+ onchange={(e) => {
32
+ const target = e.currentTarget;
33
+ if (target instanceof HTMLInputElement) {
34
+ const file = target.files?.[0];
35
+ if (file !== undefined) {
36
+ props.onChange(file);
37
+ }
38
+ }
39
+ }}
40
+ aria-describedby={hintInfo?.ariaDescribedBy}
41
+ {...ariaAttrs}
42
+ />
43
+ {#if hintInfo !== undefined}
44
+ <small id={hintInfo.id} class="sc-hint">{hintInfo.hint}</small>
45
+ {/if}
46
+ {/if}
@@ -0,0 +1,29 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for literal fields — `z.literal("a")` or
3
+ `{ const: 5 }`. Mirror of
4
+ `react/headlessRenderers.tsx :: renderLiteral`.
5
+
6
+ Literals are non-editable by nature (the value is fixed at the
7
+ schema level), so both read-only and editable modes display the
8
+ declared value(s). Multiple literals (`z.literal(["a", "b"])`)
9
+ render comma-separated through `displayJsonValue`.
10
+ -->
11
+ <script lang="ts">
12
+ import type { SvelteRenderProps } from "../types.ts";
13
+ import { fieldDomId } from "../../core/idPath.ts";
14
+ import { EM_DASH } from "../../core/cssClasses.ts";
15
+ import { displayJsonValue } from "../../core/walkBuilders.ts";
16
+
17
+ const props = $props<SvelteRenderProps>();
18
+
19
+ const id = $derived(fieldDomId(props.path));
20
+ const literalValues = $derived(
21
+ props.tree.type === "literal" ? props.tree.literalValues : []
22
+ );
23
+ </script>
24
+
25
+ {#if props.tree.type !== "literal" || literalValues.length === 0}
26
+ <span {id}>{EM_DASH}</span>
27
+ {:else}
28
+ <span {id}>{literalValues.map((v) => displayJsonValue(v)).join(", ")}</span>
29
+ {/if}
@@ -0,0 +1,24 @@
1
+ <!--
2
+ Utility component that mounts a {@link SvelteRenderDescriptor}
3
+ returned by `props.renderChild(...)`. Centralised so every
4
+ container renderer (`Object`, `Array`, `Tuple`, `Record`,
5
+ `Union`, `DiscriminatedUnion`, `Conditional`, `Negation`) uses
6
+ the same `<svelte:component>`-equivalent code path.
7
+
8
+ Svelte 5's dynamic-component feature renders a capitalised local
9
+ variable that references a `Component<Props>` constructor as
10
+ `<Var ... />`. The descriptor is destructured once here so the
11
+ consumer just writes `<Mount {descriptor} />`.
12
+ -->
13
+ <script lang="ts">
14
+ import type { SvelteRenderDescriptor } from "../types.ts";
15
+
16
+ interface Props {
17
+ descriptor: SvelteRenderDescriptor;
18
+ }
19
+
20
+ const { descriptor }: Props = $props();
21
+ const Component = $derived(descriptor.component);
22
+ </script>
23
+
24
+ <Component {...descriptor.props} />
@@ -0,0 +1,35 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for negation fields — JSON Schema
3
+ `{ not: { ... } }`. Mirror of
4
+ `react/headlessRenderers.tsx :: renderNegation`.
5
+
6
+ Negation describes a constraint ("value must NOT match this
7
+ schema") rather than a value shape. The renderer surfaces the
8
+ negated schema beneath an explanatory preamble inside a
9
+ `<fieldset>`.
10
+ -->
11
+ <script lang="ts">
12
+ import type { SvelteRenderProps } from "../types.ts";
13
+ import { SC_CLASSES } from "../../core/cssClasses.ts";
14
+ import Mount from "./Mount.svelte";
15
+
16
+ const props = $props<SvelteRenderProps>();
17
+
18
+ const negated = $derived(
19
+ props.tree.type === "negation" ? props.tree.negated : undefined
20
+ );
21
+ const child = $derived(
22
+ negated !== undefined
23
+ ? props.renderChild(negated, props.value, props.onChange)
24
+ : null
25
+ );
26
+ </script>
27
+
28
+ {#if negated !== undefined}
29
+ <fieldset class={SC_CLASSES.negation}>
30
+ <strong>Must NOT match:</strong>
31
+ {#if child !== null}
32
+ <Mount descriptor={child} />
33
+ {/if}
34
+ </fieldset>
35
+ {/if}
@@ -0,0 +1,24 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `NeverField` — `z.never()` or
3
+ `{ not: {} }` / `false` schemas. Mirror of
4
+ `react/headlessRenderers.tsx :: renderNever`.
5
+
6
+ `never` indicates a position that cannot hold any value. Render a
7
+ visible placeholder rather than throwing because some valid
8
+ schemas intentionally contain `never` branches (e.g. exhaustive
9
+ discriminated unions), and a runtime crash on render would be
10
+ worse than a visible indicator.
11
+ -->
12
+ <script lang="ts">
13
+ import type { SvelteRenderProps } from "../types.ts";
14
+ import { fieldDomId } from "../../core/idPath.ts";
15
+ import { SC_CLASSES } from "../../core/cssClasses.ts";
16
+
17
+ const props = $props<SvelteRenderProps>();
18
+
19
+ const id = $derived(fieldDomId(props.path));
20
+ </script>
21
+
22
+ <span {id} class={SC_CLASSES.never}>
23
+ <em>never matches</em>
24
+ </span>
@@ -0,0 +1,19 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for null fields — `z.null()` or
3
+ `{ type: "null" }`. Mirror of
4
+ `react/headlessRenderers.tsx :: renderNull`.
5
+
6
+ The only valid value is `null`, so render an em-dash placeholder
7
+ regardless of mode. There is nothing the user can usefully change.
8
+ -->
9
+ <script lang="ts">
10
+ import type { SvelteRenderProps } from "../types.ts";
11
+ import { fieldDomId } from "../../core/idPath.ts";
12
+ import { EM_DASH } from "../../core/cssClasses.ts";
13
+
14
+ const props = $props<SvelteRenderProps>();
15
+
16
+ const id = $derived(fieldDomId(props.path));
17
+ </script>
18
+
19
+ <span {id}>{EM_DASH}</span>
@@ -0,0 +1,68 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `NumberField` — plain
3
+ `<input type="number">` mirror of
4
+ `react/headlessRenderers.tsx :: renderNumber`.
5
+
6
+ Drives `inputmode` from the walked field's `isInteger` flag
7
+ (`numeric` for integers, `decimal` otherwise) and the step
8
+ attribute from `multipleOf` — falling back to `step="1"` for
9
+ integer schemas without `multipleOf`, omitted otherwise so the
10
+ browser default applies.
11
+
12
+ Read-only mode emits the value through `toLocaleString()` so
13
+ locale-aware thousands separators / decimal marks match the
14
+ React equivalent.
15
+ -->
16
+ <script lang="ts">
17
+ import type { SvelteRenderProps } from "../types.ts";
18
+ import { fieldDomId } from "../../core/idPath.ts";
19
+ import { EM_DASH } from "../../core/cssClasses.ts";
20
+ import { buildAriaAttrs, buildHintInfo } from "../a11y.ts";
21
+
22
+ const props = $props<SvelteRenderProps>();
23
+
24
+ const id = $derived(fieldDomId(props.path));
25
+ </script>
26
+
27
+ {#if props.readOnly}
28
+ {#if typeof props.value !== "number"}
29
+ <span {id}>{EM_DASH}</span>
30
+ {:else}
31
+ <span {id}>{props.value.toLocaleString()}</span>
32
+ {/if}
33
+ {:else}
34
+ {@const numValue = typeof props.value === "number" ? props.value : ""}
35
+ {@const ariaAttrs = buildAriaAttrs(props.tree)}
36
+ {@const hintInfo = buildHintInfo(id, props.constraints)}
37
+ {@const isInteger =
38
+ props.tree.type === "number" ? props.tree.isInteger : false}
39
+ {@const inputMode = isInteger ? "numeric" : "decimal"}
40
+ {@const multipleOf = props.constraints.multipleOf}
41
+ {@const step =
42
+ multipleOf !== undefined
43
+ ? String(multipleOf)
44
+ : isInteger
45
+ ? "1"
46
+ : undefined}
47
+
48
+ <input
49
+ {id}
50
+ type="number"
51
+ inputmode={inputMode}
52
+ {step}
53
+ value={props.writeOnly ? "" : numValue}
54
+ onchange={(e) => {
55
+ const target = e.currentTarget;
56
+ if (target instanceof HTMLInputElement) {
57
+ props.onChange(Number(target.value));
58
+ }
59
+ }}
60
+ min={props.constraints.minimum}
61
+ max={props.constraints.maximum}
62
+ aria-describedby={hintInfo?.ariaDescribedBy}
63
+ {...ariaAttrs}
64
+ />
65
+ {#if hintInfo !== undefined}
66
+ <small id={hintInfo.id} class="sc-hint">{hintInfo.hint}</small>
67
+ {/if}
68
+ {/if}
@@ -0,0 +1,74 @@
1
+ <!--
2
+ Headless Svelte 5 renderer for `ObjectField` — `<fieldset>` per
3
+ object with one labelled child per property. Mirrors
4
+ `react/headlessRenderers.tsx :: renderObject`.
5
+
6
+ Each property is rendered by calling `props.renderChild(field,
7
+ childValue, childOnChange, key)` and mounting the returned
8
+ descriptor via `<Mount descriptor={…} />` (the shared utility
9
+ component in this directory).
10
+ The label text falls back to the structural key when no
11
+ `meta.description` is supplied — every input gets an accessible
12
+ name even on an undecorated `z.object({...})`.
13
+ -->
14
+ <script lang="ts">
15
+ import type { SvelteRenderProps } from "../types.ts";
16
+ import { fieldDomId } from "../../core/idPath.ts";
17
+ import { sortFieldsByOrder } from "../../core/fieldOrder.ts";
18
+ import { isObject } from "../../core/guards.ts";
19
+ import Mount from "./Mount.svelte";
20
+
21
+ const props = $props<SvelteRenderProps>();
22
+
23
+ /**
24
+ * Narrow once at the top — the dispatcher only calls this renderer
25
+ * when `tree.type === "object"`. The check is defensive against
26
+ * misconfigured custom resolvers wiring this component into the
27
+ * wrong slot.
28
+ */
29
+ const fields = $derived(
30
+ props.tree.type === "object" ? props.tree.fields : {}
31
+ );
32
+
33
+ const obj = $derived<Record<string, unknown>>(
34
+ isObject(props.value) ? props.value : {}
35
+ );
36
+
37
+ const sortedEntries = $derived(sortFieldsByOrder(fields));
38
+ </script>
39
+
40
+ <fieldset>
41
+ {#if typeof props.meta.description === "string"}
42
+ <legend>{props.meta.description}</legend>
43
+ {/if}
44
+ {#each sortedEntries.filter(([, field]) => field.meta.visible !== false) as [key, field] (key)}
45
+ {@const childValue = obj[key]}
46
+ {@const childId = fieldDomId(`${props.path}.${key}`)}
47
+ {@const labelText =
48
+ typeof field.meta.description === "string"
49
+ ? field.meta.description
50
+ : key}
51
+ {@const childOnChange = (v: unknown) => {
52
+ const updated: Record<string, unknown> = {};
53
+ for (const [k, val] of Object.entries(obj)) {
54
+ updated[k] = val;
55
+ }
56
+ updated[key] = v;
57
+ props.onChange(updated);
58
+ }}
59
+ {@const child = props.renderChild(field, childValue, childOnChange, key)}
60
+ {#if child !== null}
61
+ <div>
62
+ <label for={childId}>
63
+ {labelText}
64
+ {#if field.isOptional === false}
65
+ <span aria-hidden="true" style="color: #dc2626"
66
+ >{" *"}</span
67
+ >
68
+ {/if}
69
+ </label>
70
+ <Mount descriptor={child} />
71
+ </div>
72
+ {/if}
73
+ {/each}
74
+ </fieldset>