schema-components 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -1
- package/dist/SchemaComponent-B__6-5-E.d.mts +277 -0
- package/dist/SchemaComponent-BxzzsHsK.mjs +668 -0
- package/dist/adapter-ktQaheWB.d.mts +213 -0
- package/dist/constructorTypes-BdCiMS6e.d.mts +30 -0
- package/dist/core/adapter.d.mts +3 -213
- package/dist/core/constraintHint.d.mts +1 -1
- package/dist/core/constraints.d.mts +2 -2
- package/dist/core/contexts.d.mts +71 -0
- package/dist/core/contexts.mjs +1 -0
- package/dist/core/diagnostics.d.mts +1 -1
- package/dist/core/errors.d.mts +1 -1
- package/dist/core/fieldOrder.d.mts +1 -1
- package/dist/{react → core}/fieldPath.d.mts +2 -2
- package/dist/{react → core}/fieldPath.mjs +3 -3
- package/dist/core/formats.d.mts +1 -1
- package/dist/core/inferValue.d.mts +1 -1
- package/dist/core/limits.d.mts +1 -1
- package/dist/core/merge.d.mts +1 -1
- package/dist/core/normalise.d.mts +2 -2
- package/dist/core/ref.d.mts +1 -1
- package/dist/core/renderField.d.mts +147 -0
- package/dist/core/renderField.mjs +81 -0
- package/dist/core/renderer.d.mts +2 -199
- package/dist/core/swagger2.d.mts +1 -1
- package/dist/core/typeInference.d.mts +1 -982
- package/dist/core/types.d.mts +1 -1
- package/dist/core/unionMatch.d.mts +1 -1
- package/dist/core/version.d.mts +1 -1
- package/dist/core/walkBuilders.d.mts +3 -3
- package/dist/core/walker.d.mts +1 -1
- package/dist/{errors-Dki7tji4.d.mts → errors-DbaI04x2.d.mts} +1 -1
- package/dist/html/a11y.d.mts +2 -2
- package/dist/html/renderToHtml.d.mts +5 -5
- package/dist/html/renderToHtml.mjs +33 -18
- package/dist/html/renderToHtmlStream.d.mts +5 -5
- package/dist/html/renderers.d.mts +1 -1
- package/dist/html/streamRenderers.d.mts +3 -3
- package/dist/{inferValue-Ce-PviSD.d.mts → inferValue-eAnh50EM.d.mts} +3 -3
- package/dist/lit/SchemaComponent.d.mts +125 -0
- package/dist/lit/SchemaComponent.mjs +2 -0
- package/dist/lit/SchemaField.d.mts +65 -0
- package/dist/lit/SchemaField.mjs +2 -0
- package/dist/lit/SchemaView.d.mts +14 -0
- package/dist/lit/SchemaView.mjs +2 -0
- package/dist/lit/constructorTypes.d.mts +2 -0
- package/dist/lit/constructorTypes.mjs +1 -0
- package/dist/lit/contexts.d.mts +78 -0
- package/dist/lit/contexts.mjs +238 -0
- package/dist/lit/defaultResolver.d.mts +33 -0
- package/dist/lit/defaultResolver.mjs +2 -0
- package/dist/lit/registry.d.mts +66 -0
- package/dist/lit/registry.mjs +2 -0
- package/dist/lit/renderers/baseElement.d.mts +131 -0
- package/dist/lit/renderers/baseElement.mjs +109 -0
- package/dist/lit/renderers/recordHelpers.d.mts +25 -0
- package/dist/lit/renderers/recordHelpers.mjs +55 -0
- package/dist/lit/renderers/scArray.d.mts +14 -0
- package/dist/lit/renderers/scArray.mjs +86 -0
- package/dist/lit/renderers/scBoolean.d.mts +15 -0
- package/dist/lit/renderers/scBoolean.mjs +47 -0
- package/dist/lit/renderers/scConditional.d.mts +23 -0
- package/dist/lit/renderers/scConditional.mjs +65 -0
- package/dist/lit/renderers/scDiscriminated.d.mts +23 -0
- package/dist/lit/renderers/scDiscriminated.mjs +138 -0
- package/dist/lit/renderers/scEnum.d.mts +16 -0
- package/dist/lit/renderers/scEnum.mjs +66 -0
- package/dist/lit/renderers/scFile.d.mts +15 -0
- package/dist/lit/renderers/scFile.mjs +53 -0
- package/dist/lit/renderers/scLiteralNullNever.d.mts +30 -0
- package/dist/lit/renderers/scLiteralNullNever.mjs +57 -0
- package/dist/lit/renderers/scNumber.d.mts +15 -0
- package/dist/lit/renderers/scNumber.mjs +64 -0
- package/dist/lit/renderers/scObject.d.mts +14 -0
- package/dist/lit/renderers/scObject.mjs +57 -0
- package/dist/lit/renderers/scRecord.d.mts +14 -0
- package/dist/lit/renderers/scRecord.mjs +112 -0
- package/dist/lit/renderers/scString.d.mts +19 -0
- package/dist/lit/renderers/scString.mjs +165 -0
- package/dist/lit/renderers/scTuple.d.mts +14 -0
- package/dist/lit/renderers/scTuple.mjs +58 -0
- package/dist/lit/renderers/scUnion.d.mts +14 -0
- package/dist/lit/renderers/scUnion.mjs +44 -0
- package/dist/lit/renderers/scUnknown.d.mts +15 -0
- package/dist/lit/renderers/scUnknown.mjs +45 -0
- package/dist/lit/ssr.d.mts +37 -0
- package/dist/lit/ssr.mjs +9565 -0
- package/dist/lit/types.d.mts +2 -0
- package/dist/lit/types.mjs +1 -0
- package/dist/lit/widget.d.mts +71 -0
- package/dist/lit/widget.mjs +87 -0
- package/dist/openapi/ApiCallbacks.d.mts +1 -1
- package/dist/openapi/ApiLinks.d.mts +1 -1
- package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
- package/dist/openapi/ApiSecurity.d.mts +1 -1
- package/dist/openapi/components.d.mts +4 -4
- package/dist/openapi/parser.d.mts +2 -2
- package/dist/openapi/resolve.d.mts +1 -1
- package/dist/preact/SchemaComponent.d.mts +3 -0
- package/dist/preact/SchemaComponent.mjs +26 -0
- package/dist/preact/SchemaErrorBoundary.d.mts +2 -0
- package/dist/preact/SchemaErrorBoundary.mjs +20 -0
- package/dist/preact/SchemaView.d.mts +2 -0
- package/dist/preact/SchemaView.mjs +22 -0
- package/dist/preact/headless.d.mts +2 -0
- package/dist/preact/headless.mjs +18 -0
- package/dist/react/SchemaComponent.d.mts +3 -270
- package/dist/react/SchemaComponent.mjs +41 -32
- package/dist/react/SchemaView.d.mts +6 -6
- package/dist/react/SchemaView.mjs +32 -29
- package/dist/react/a11y.d.mts +2 -2
- package/dist/react/fieldShell.d.mts +1 -1
- package/dist/react/headless.d.mts +1 -1
- package/dist/react/headlessRenderers.d.mts +2 -2
- package/dist/{ref-DdsbekXX.d.mts → ref-DWrQG1Er.d.mts} +1 -1
- package/dist/renderer-ab9E52Bp.d.mts +245 -0
- package/dist/solid/SchemaComponent.d.mts +136 -0
- package/dist/solid/SchemaComponent.mjs +391 -0
- package/dist/solid/SchemaErrorBoundary.d.mts +38 -0
- package/dist/solid/SchemaErrorBoundary.mjs +57 -0
- package/dist/solid/SchemaField.d.mts +40 -0
- package/dist/solid/SchemaField.mjs +113 -0
- package/dist/solid/SchemaView.d.mts +54 -0
- package/dist/solid/SchemaView.mjs +168 -0
- package/dist/solid/a11y.d.mts +70 -0
- package/dist/solid/a11y.mjs +71 -0
- package/dist/solid/contexts.d.mts +37 -0
- package/dist/solid/contexts.mjs +66 -0
- package/dist/solid/headless.d.mts +10 -0
- package/dist/solid/headless.mjs +27 -0
- package/dist/solid/renderers.d.mts +79 -0
- package/dist/solid/renderers.mjs +840 -0
- package/dist/solid/types.d.mts +90 -0
- package/dist/solid/types.mjs +1 -0
- package/dist/solid/widget.d.mts +29 -0
- package/dist/solid/widget.mjs +35 -0
- package/dist/themes/mantine.d.mts +1 -1
- package/dist/themes/mui.d.mts +1 -1
- package/dist/themes/radix.d.mts +1 -1
- package/dist/themes/shadcn.d.mts +1 -1
- package/dist/typeInference-Y8tNEQJk.d.mts +983 -0
- package/dist/types-BCy7K3nk.d.mts +125 -0
- package/package.json +71 -1
- package/src/svelte/SchemaComponent.svelte +427 -0
- package/src/svelte/SchemaErrorBoundary.svelte +66 -0
- package/src/svelte/SchemaField.svelte +216 -0
- package/src/svelte/SchemaProvider.svelte +46 -0
- package/src/svelte/SchemaView.svelte +244 -0
- package/src/svelte/a11y.ts +112 -0
- package/src/svelte/contexts.ts +79 -0
- package/src/svelte/dispatch.ts +267 -0
- package/src/svelte/headless.ts +73 -0
- package/src/svelte/headlessFns.ts +124 -0
- package/src/svelte/renderers/Array.svelte +98 -0
- package/src/svelte/renderers/Boolean.svelte +43 -0
- package/src/svelte/renderers/Conditional.svelte +67 -0
- package/src/svelte/renderers/DiscriminatedUnion.svelte +197 -0
- package/src/svelte/renderers/Enum.svelte +53 -0
- package/src/svelte/renderers/Fallback.svelte +24 -0
- package/src/svelte/renderers/File.svelte +46 -0
- package/src/svelte/renderers/Literal.svelte +29 -0
- package/src/svelte/renderers/Mount.svelte +24 -0
- package/src/svelte/renderers/Negation.svelte +35 -0
- package/src/svelte/renderers/Never.svelte +24 -0
- package/src/svelte/renderers/Null.svelte +19 -0
- package/src/svelte/renderers/Number.svelte +68 -0
- package/src/svelte/renderers/Object.svelte +74 -0
- package/src/svelte/renderers/Record.svelte +134 -0
- package/src/svelte/renderers/RecursionSentinel.svelte +27 -0
- package/src/svelte/renderers/String.svelte +152 -0
- package/src/svelte/renderers/Tuple.svelte +84 -0
- package/src/svelte/renderers/Union.svelte +49 -0
- package/src/svelte/renderers/Unknown.svelte +42 -0
- package/src/svelte/svelte-modules.d.ts +25 -0
- package/src/svelte/types.ts +238 -0
- package/src/svelte/widget.ts +62 -0
- /package/dist/{diagnostics-BTrm3O6J.d.mts → diagnostics-mftUZI7c.d.mts} +0 -0
- /package/dist/{limits-x4OiyJxh.d.mts → limits-Vv9hUbI_.d.mts} +0 -0
- /package/dist/{types-BrYbjC7_.d.mts → types-BBQaEPfE.d.mts} +0 -0
- /package/dist/{version-DL8U5RuA.d.mts → version-BEBx10ND.d.mts} +0 -0
|
@@ -0,0 +1,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>
|