schema-components 2.1.0 → 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.
@@ -0,0 +1,178 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<SchemaField>` — render a single field from a schema by dot-separated
4
+ * path.
5
+ *
6
+ * Vue counterpart of `react/SchemaComponent.tsx`'s `<SchemaField>`.
7
+ * Walks the full schema tree and resolves the field at the supplied
8
+ * `path`, then renders only that field through the same Vue resolver
9
+ * pipeline as `<SchemaComponent>`.
10
+ *
11
+ * Useful for embedding individual fields inside bespoke layouts (e.g.
12
+ * a custom Vue form that lays out address fields manually but still
13
+ * wants the schema-driven rendering for each).
14
+ *
15
+ * @group Components
16
+ */
17
+ import { computed, toRaw, type VNode } from "vue";
18
+ import { walk } from "../core/walker.ts";
19
+ import type { WalkOptions } from "../core/walkBuilders.ts";
20
+ import { normaliseSchema } from "../core/adapter.ts";
21
+ import {
22
+ resolvePath,
23
+ resolveValue,
24
+ setNestedValue,
25
+ } from "../core/fieldPath.ts";
26
+ import type { SchemaMeta, WalkedField } from "../core/types.ts";
27
+ import {
28
+ SchemaFieldError,
29
+ SchemaNormalisationError,
30
+ } from "../core/errors.ts";
31
+ import { VueResolverContext, VueWidgetsContext } from "./contexts.ts";
32
+ import { vueRenderField } from "./renderField.ts";
33
+ import { deriveIdPrefix, joinPath } from "./idPrefix.ts";
34
+ import type { VueRenderProps } from "./types.ts";
35
+ import { VNodeHost } from "./VNodeHost.ts";
36
+
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ /** Dot-separated path to the field (e.g. `"address.city"`). */
40
+ path: string;
41
+ /** The schema to extract the field from. */
42
+ schema: unknown;
43
+ /** For OpenAPI: a ref string. */
44
+ refPath?: string;
45
+ /** Current value of the root object the field belongs to. */
46
+ modelValue?: unknown;
47
+ /** Explicit onChange callback. Wired alongside `update:modelValue`. */
48
+ onChange?: (value: unknown) => void;
49
+ /** Override meta for this specific field. */
50
+ meta?: SchemaMeta;
51
+ /** Deterministic id prefix. Defaults to a per-instance `useId()` value. */
52
+ idPrefix?: string;
53
+ }>(),
54
+ {
55
+ refPath: undefined,
56
+ modelValue: undefined,
57
+ onChange: undefined,
58
+ meta: () => ({}),
59
+ idPrefix: undefined,
60
+ }
61
+ );
62
+
63
+ const emit = defineEmits<{
64
+ "update:modelValue": [value: unknown];
65
+ change: [value: unknown];
66
+ }>();
67
+
68
+ const contextResolver = VueResolverContext.consume();
69
+ const contextWidgets = VueWidgetsContext.consume();
70
+
71
+ interface Normalised {
72
+ jsonSchema: Record<string, unknown>;
73
+ rootMeta: SchemaMeta | undefined;
74
+ rootDocument: Record<string, unknown>;
75
+ }
76
+
77
+ const normalised = computed<Normalised>(() => {
78
+ try {
79
+ // See the matching `toRaw` note in `SchemaComponent.vue` —
80
+ // Zod schemas carry non-configurable members that Vue's
81
+ // default reactive Proxy cannot mirror.
82
+ const rawSchema = toRaw(props.schema);
83
+ const result = normaliseSchema(rawSchema, props.refPath);
84
+ return {
85
+ jsonSchema: result.jsonSchema,
86
+ rootMeta: result.rootMeta,
87
+ rootDocument: result.rootDocument,
88
+ };
89
+ } catch (err) {
90
+ if (err instanceof SchemaNormalisationError) throw err;
91
+ throw new SchemaNormalisationError(
92
+ err instanceof Error ? err.message : "Failed to normalise schema",
93
+ toRaw(props.schema),
94
+ "unknown"
95
+ );
96
+ }
97
+ });
98
+
99
+ const fullTree = computed<WalkedField>(() => {
100
+ const n = normalised.value;
101
+ const walkOptions: WalkOptions = {
102
+ componentMeta: props.meta,
103
+ rootDocument: n.rootDocument,
104
+ };
105
+ if (n.rootMeta !== undefined) walkOptions.rootMeta = n.rootMeta;
106
+ return walk(n.jsonSchema, walkOptions);
107
+ });
108
+
109
+ const fieldTree = computed<WalkedField>(() => {
110
+ const found = resolvePath(fullTree.value, props.path);
111
+ if (found === undefined) {
112
+ throw new SchemaFieldError(
113
+ `Field not found: ${props.path}`,
114
+ toRaw(props.schema),
115
+ props.path
116
+ );
117
+ }
118
+ return found;
119
+ });
120
+
121
+ const fieldValue = computed<unknown>(() =>
122
+ resolveValue(props.modelValue, props.path)
123
+ );
124
+
125
+ const rootBase = computed(() => deriveIdPrefix(props.idPrefix));
126
+ const rootPath = computed(() => joinPath(rootBase.value, props.path));
127
+
128
+ function handleFieldChange(nextField: unknown): void {
129
+ const newRoot = setNestedValue(props.modelValue, props.path, nextField);
130
+ emit("update:modelValue", newRoot);
131
+ emit("change", newRoot);
132
+ props.onChange?.(newRoot);
133
+ }
134
+
135
+ function makeRenderChild(
136
+ currentDepth: number,
137
+ parentPath: string
138
+ ): VueRenderProps["renderChild"] {
139
+ return (
140
+ childTree: WalkedField,
141
+ childValue: unknown,
142
+ childOnChange: (v: unknown) => void,
143
+ pathSuffix?: string
144
+ ) => {
145
+ const childPath = joinPath(parentPath, pathSuffix);
146
+ return vueRenderField(
147
+ childTree,
148
+ childValue,
149
+ childOnChange,
150
+ contextResolver,
151
+ makeRenderChild(currentDepth + 1, childPath),
152
+ childPath,
153
+ undefined,
154
+ contextWidgets,
155
+ currentDepth + 1
156
+ );
157
+ };
158
+ }
159
+
160
+ const rootVNode = computed<VNode>(() => {
161
+ const renderChild = makeRenderChild(0, rootPath.value);
162
+ return vueRenderField(
163
+ fieldTree.value,
164
+ fieldValue.value,
165
+ handleFieldChange,
166
+ contextResolver,
167
+ renderChild,
168
+ rootPath.value,
169
+ undefined,
170
+ contextWidgets,
171
+ 0
172
+ );
173
+ });
174
+ </script>
175
+
176
+ <template>
177
+ <VNodeHost :node="rootVNode" />
178
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<SchemaProvider>` — provide a theme resolver and scoped widgets to
4
+ * every `<SchemaComponent>` and `<SchemaView>` rendered inside the
5
+ * subtree.
6
+ *
7
+ * Vue counterpart of the React adapter's `SchemaProvider`. Uses the
8
+ * abstract `ContextPort` from `core/contexts.ts` (instantiated as
9
+ * `VueResolverContext` / `VueWidgetsContext` via Vue's `provide` /
10
+ * `inject`) so the dispatcher remains decoupled from Vue specifics —
11
+ * the only Vue-specific code is the `provide()` call here and the
12
+ * matching `inject()` calls inside the consumer SFCs.
13
+ *
14
+ * @group Components
15
+ */
16
+ import { VueResolverContext, VueWidgetsContext } from "./contexts.ts";
17
+ import type { VueComponentResolver, VueWidgetMap } from "./types.ts";
18
+
19
+ const props = defineProps<{
20
+ /** The theme adapter that drives every nested `<SchemaComponent>`. */
21
+ resolver: VueComponentResolver;
22
+ /** Scoped widgets available to descendants. */
23
+ widgets?: VueWidgetMap;
24
+ }>();
25
+
26
+ // Provide both contexts before children render. `VueResolverContext.provide`
27
+ // wraps Vue's `provide()` so the call site does not depend directly on
28
+ // Vue's primitive — the abstraction lives in `core/contexts.ts`.
29
+ // `null` is passed as `children` because Vue's provide model attaches
30
+ // values to the component instance rather than wrapping a children
31
+ // subtree; the port signature keeps the argument for React-adapter
32
+ // compatibility.
33
+ VueResolverContext.provide(props.resolver, null);
34
+ VueWidgetsContext.provide(props.widgets, null);
35
+ </script>
36
+
37
+ <template>
38
+ <slot />
39
+ </template>
@@ -0,0 +1,198 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<SchemaView>` — read-only Vue renderer.
4
+ *
5
+ * Vue counterpart of `react/SchemaView.tsx`. Always renders read-only
6
+ * output; the dispatch loop is identical to `<SchemaComponent>` but
7
+ * the `onChange` callback handed to the dispatcher is a noop and
8
+ * `mergedMeta.readOnly` is forced to `true`.
9
+ *
10
+ * SSR story: Vue ships a server renderer (`@vue/server-renderer`)
11
+ * that emits HTML strings from the same render functions, so this
12
+ * SFC is safe to use inside a Nuxt server component or a custom
13
+ * `renderToString` pipeline. Unlike the React Server Component
14
+ * version, there is no separate RSC restriction — Vue components
15
+ * have a single rendering model that works in both environments.
16
+ *
17
+ * @group Components
18
+ */
19
+ import { computed, h, toRaw, type VNode } from "vue";
20
+ import { walk } from "../core/walker.ts";
21
+ import type { WalkOptions } from "../core/walkBuilders.ts";
22
+ import { normaliseSchema } from "../core/adapter.ts";
23
+ import type { SchemaMeta, WalkedField } from "../core/types.ts";
24
+ import type { Diagnostic, DiagnosticsOptions } from "../core/diagnostics.ts";
25
+ import { SchemaNormalisationError } from "../core/errors.ts";
26
+ import { toRecordOrUndefined } from "../core/guards.ts";
27
+ import { vueRenderField } from "./renderField.ts";
28
+ import { deriveIdPrefix, joinPath } from "./idPrefix.ts";
29
+ import type {
30
+ VueComponentResolver,
31
+ VueRenderProps,
32
+ VueWidgetMap,
33
+ } from "./types.ts";
34
+ import { VNodeHost } from "./VNodeHost.ts";
35
+
36
+ const props = withDefaults(
37
+ defineProps<{
38
+ /** Zod schema, JSON Schema object, or OpenAPI document. */
39
+ schema: unknown;
40
+ /** For OpenAPI: a ref string. */
41
+ refPath?: string;
42
+ /** Current value to render. */
43
+ value?: unknown;
44
+ /** Meta overrides applied to the root schema. */
45
+ meta?: SchemaMeta;
46
+ /** Convenience: sets `description` on the root. */
47
+ description?: string;
48
+ /** Per-field meta overrides — nested object mirroring schema shape. */
49
+ fields?: Record<string, unknown>;
50
+ /**
51
+ * Theme resolver. In a Server Component environment you pass
52
+ * this explicitly because the context-based `<SchemaProvider>`
53
+ * may not be mounted on the server.
54
+ */
55
+ resolver?: VueComponentResolver;
56
+ /** Instance-scoped widgets. */
57
+ widgets?: VueWidgetMap;
58
+ /** Deterministic id prefix. Defaults to a per-instance `useId()` value. */
59
+ idPrefix?: string;
60
+ /** Called with each diagnostic emitted during schema processing. */
61
+ onDiagnostic?: (diagnostic: Diagnostic) => void;
62
+ /** When `true`, any diagnostic becomes a thrown error. */
63
+ strict?: boolean;
64
+ }>(),
65
+ {
66
+ refPath: undefined,
67
+ value: undefined,
68
+ meta: () => ({}),
69
+ description: undefined,
70
+ fields: undefined,
71
+ resolver: undefined,
72
+ widgets: undefined,
73
+ idPrefix: undefined,
74
+ onDiagnostic: undefined,
75
+ strict: false,
76
+ }
77
+ );
78
+
79
+ const rootPath = computed(() => deriveIdPrefix(props.idPrefix));
80
+
81
+ const mergedMeta = computed<SchemaMeta>(() => {
82
+ const merged: SchemaMeta = { ...props.meta, readOnly: true };
83
+ if (props.description !== undefined) merged.description = props.description;
84
+ return merged;
85
+ });
86
+
87
+ const diagnostics = computed<DiagnosticsOptions | undefined>(() => {
88
+ if (props.onDiagnostic === undefined && !props.strict) return undefined;
89
+ const opts: DiagnosticsOptions = {};
90
+ if (props.onDiagnostic !== undefined) opts.diagnostics = props.onDiagnostic;
91
+ if (props.strict) opts.strict = true;
92
+ return opts;
93
+ });
94
+
95
+ interface Normalised {
96
+ jsonSchema: Record<string, unknown>;
97
+ rootMeta: SchemaMeta | undefined;
98
+ rootDocument: Record<string, unknown>;
99
+ }
100
+
101
+ const normalised = computed<Normalised>(() => {
102
+ const opts =
103
+ diagnostics.value !== undefined
104
+ ? { diagnostics: diagnostics.value }
105
+ : undefined;
106
+ try {
107
+ // `toRaw` peels off Vue's reactive proxy before the schema
108
+ // crosses into `normaliseSchema`, which expects the bare
109
+ // object (Zod schemas in particular have non-configurable
110
+ // `_zod` data members that Vue's default Proxy cannot mirror).
111
+ // See the matching comment in `SchemaComponent.vue`.
112
+ const rawSchema = toRaw(props.schema);
113
+ const result = normaliseSchema(rawSchema, props.refPath, opts);
114
+ return {
115
+ jsonSchema: result.jsonSchema,
116
+ rootMeta: result.rootMeta,
117
+ rootDocument: result.rootDocument,
118
+ };
119
+ } catch (err) {
120
+ if (err instanceof SchemaNormalisationError) throw err;
121
+ throw new SchemaNormalisationError(
122
+ err instanceof Error ? err.message : "Failed to normalise schema",
123
+ toRaw(props.schema),
124
+ "unknown"
125
+ );
126
+ }
127
+ });
128
+
129
+ const tree = computed<WalkedField>(() => {
130
+ const n = normalised.value;
131
+ const walkOptions: WalkOptions = {
132
+ componentMeta: mergedMeta.value,
133
+ rootDocument: n.rootDocument,
134
+ };
135
+ if (n.rootMeta !== undefined) walkOptions.rootMeta = n.rootMeta;
136
+ const fieldsRecord = toRecordOrUndefined(props.fields);
137
+ if (fieldsRecord !== undefined) walkOptions.fieldOverrides = fieldsRecord;
138
+ if (diagnostics.value !== undefined)
139
+ walkOptions.diagnostics = diagnostics.value;
140
+ return walk(n.jsonSchema, walkOptions);
141
+ });
142
+
143
+ /** Noop onChange — SchemaView never propagates value changes. */
144
+ function noopChange(): void {
145
+ /* intentional no-op */
146
+ }
147
+
148
+ function makeRenderChild(
149
+ currentDepth: number,
150
+ parentPath: string
151
+ ): VueRenderProps["renderChild"] {
152
+ return (
153
+ childTree: WalkedField,
154
+ childValue: unknown,
155
+ _childOnChange: (v: unknown) => void,
156
+ pathSuffix?: string
157
+ ) => {
158
+ const childPath = joinPath(parentPath, pathSuffix);
159
+ return vueRenderField(
160
+ childTree,
161
+ childValue,
162
+ noopChange,
163
+ props.resolver,
164
+ makeRenderChild(currentDepth + 1, childPath),
165
+ childPath,
166
+ props.widgets,
167
+ undefined,
168
+ currentDepth + 1
169
+ );
170
+ };
171
+ }
172
+
173
+ const rootVNode = computed<VNode>(() => {
174
+ const t = tree.value;
175
+ const renderChild = makeRenderChild(0, rootPath.value);
176
+ return vueRenderField(
177
+ t,
178
+ props.value ?? t.defaultValue,
179
+ noopChange,
180
+ props.resolver,
181
+ renderChild,
182
+ rootPath.value,
183
+ props.widgets,
184
+ undefined,
185
+ 0
186
+ );
187
+ });
188
+
189
+ // Silence the `h` import: kept available for downstream theme adapters
190
+ // that wrap `<SchemaView>` and need to compose extra structure around
191
+ // the render output. The SFC body itself only consumes the computed
192
+ // VNode through `<VNodeHost>`.
193
+ void h;
194
+ </script>
195
+
196
+ <template>
197
+ <VNodeHost :node="rootVNode" />
198
+ </template>
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Functional Vue component whose sole job is to render a supplied
3
+ * `VNode` as its output. The SFC entry points (`SchemaComponent.vue`,
4
+ * `SchemaView.vue`, `SchemaField.vue`) compute their render output as
5
+ * a VNode in `setup()` and pass it through a `&lt;VNodeHost :node="vnode" /&gt;`
6
+ * tag so the template body remains a single declarative anchor while
7
+ * the actual rendering work happens in the dispatcher.
8
+ *
9
+ * Why this exists: Vue's `&lt;component :is&gt;` directive expects a
10
+ * component definition or a tag name string — it does not accept a
11
+ * bare `VNode`. Returning a render function from a `&lt;script setup&gt;`
12
+ * block is also not a Vue idiom (the SFC compiler treats the setup
13
+ * block's exposed bindings as values to surface to the template). A
14
+ * functional component is the idiomatic seam: it accepts a `node`
15
+ * prop and returns it from its render function unchanged.
16
+ *
17
+ * Functional components in Vue 3 are pure render functions — no
18
+ * lifecycle, no reactive state — so the only cost is the function
19
+ * call itself; reactivity continues to flow through the parent SFC's
20
+ * computed VNode.
21
+ */
22
+
23
+ import type { FunctionalComponent, VNode } from "vue";
24
+
25
+ export const VNodeHost: FunctionalComponent<{ node: VNode }> = (props) => {
26
+ return props.node;
27
+ };
28
+
29
+ // Vue's runtime reads `props` off functional components for prop
30
+ // validation; declaring them keeps the warning-free contract that the
31
+ // SFC compiler enforces.
32
+ VNodeHost.props = ["node"];
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Vue implementations of the abstract {@link ContextPort} from
3
+ * `core/contexts.ts`.
4
+ *
5
+ * Each adapter implements the provide / consume port against its native
6
+ * context primitive. The Vue 3 implementation uses Vue's `provide` and
7
+ * `inject` (https://vuejs.org/guide/components/provide-inject.html)
8
+ * keyed by a unique {@link InjectionKey} {@link Symbol} per context so
9
+ * the two contexts never collide.
10
+ *
11
+ * `provide(value, children)` is called from inside a component's
12
+ * `setup()` (typically the `<SchemaProvider>` SFC) — Vue's `provide`
13
+ * works on the current component instance and makes the value available
14
+ * to every descendant calling `inject` with the same key. The `children`
15
+ * argument is therefore unused by the Vue port (no provider component
16
+ * is wrapped around children) but kept for {@link ContextPort}
17
+ * compatibility with React-style ports that DO wrap children. Callers
18
+ * outside a `setup()` (e.g. ad-hoc render functions) should still go
19
+ * through this port so future framework changes flow through one place.
20
+ */
21
+
22
+ import { inject, provide, type InjectionKey } from "vue";
23
+ import type {
24
+ ContextPort,
25
+ ResolverContextShape,
26
+ WidgetsContextShape,
27
+ } from "../core/contexts.ts";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Injection keys
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Vue {@link InjectionKey} for the active {@link ResolverContextShape}.
35
+ *
36
+ * Exported so theme adapters and downstream Vue components can read or
37
+ * provide the resolver directly through Vue's `inject` / `provide` —
38
+ * the {@link VueResolverContext} port wraps these calls but the raw
39
+ * symbol is available for callers that need custom composition (e.g.
40
+ * a Pinia store that wants to read the active resolver in an action).
41
+ */
42
+ export const VUE_RESOLVER_KEY: InjectionKey<ResolverContextShape> = Symbol(
43
+ "schema-components.vue.resolver"
44
+ );
45
+
46
+ /**
47
+ * Vue {@link InjectionKey} for the active {@link WidgetsContextShape}.
48
+ *
49
+ * Exported so callers can read or provide the scoped widget map directly
50
+ * through Vue's `inject` / `provide`.
51
+ */
52
+ export const VUE_WIDGETS_KEY: InjectionKey<WidgetsContextShape> = Symbol(
53
+ "schema-components.vue.widgets"
54
+ );
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Ports
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Vue implementation of {@link ContextPort} for the
62
+ * {@link ResolverContextShape}.
63
+ *
64
+ * `provide(value, _children)` calls Vue's `provide(VUE_RESOLVER_KEY, value)`
65
+ * on the current component instance. The `_children` argument is unused
66
+ * — Vue's provide model attaches the value to the component instance
67
+ * rather than wrapping children in a new component — but kept for
68
+ * {@link ContextPort} compatibility. Returns `undefined` because Vue's
69
+ * `provide` does not produce a renderable wrapper.
70
+ *
71
+ * `consume()` calls `inject(VUE_RESOLVER_KEY, undefined)` and returns
72
+ * the active resolver, or `undefined` when no provider is mounted in
73
+ * scope (matching the React adapter's `undefined`-default behaviour).
74
+ *
75
+ * Must be called from inside a component's `setup()` — Vue's `provide`
76
+ * and `inject` both rely on the current component instance, which is
77
+ * only available during component setup.
78
+ */
79
+ export const VueResolverContext: ContextPort<ResolverContextShape> = {
80
+ provide(value: ResolverContextShape, children: unknown): unknown {
81
+ provide(VUE_RESOLVER_KEY, value);
82
+ // `children` is unused — Vue's `provide()` attaches the value
83
+ // to the component instance rather than wrapping a children
84
+ // subtree — but the {@link ContextPort} signature carries it
85
+ // for React-style adapters that DO wrap children. `void`
86
+ // discards the value without triggering the unused-args rule.
87
+ void children;
88
+ return undefined;
89
+ },
90
+ consume(): ResolverContextShape {
91
+ return inject(VUE_RESOLVER_KEY, undefined);
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Vue implementation of {@link ContextPort} for the
97
+ * {@link WidgetsContextShape}. Mirrors {@link VueResolverContext}: a
98
+ * thin wrapper around Vue's `provide` / `inject` keyed by
99
+ * {@link VUE_WIDGETS_KEY}.
100
+ *
101
+ * Must be called from inside a component's `setup()`.
102
+ */
103
+ export const VueWidgetsContext: ContextPort<WidgetsContextShape> = {
104
+ provide(value: WidgetsContextShape, children: unknown): unknown {
105
+ provide(VUE_WIDGETS_KEY, value);
106
+ // See the matching `void children` note in
107
+ // {@link VueResolverContext} — Vue's `provide()` does not wrap
108
+ // children, but the port keeps the argument for React-style
109
+ // adapter compatibility.
110
+ void children;
111
+ return undefined;
112
+ },
113
+ consume(): WidgetsContextShape {
114
+ return inject(VUE_WIDGETS_KEY, undefined);
115
+ },
116
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Narrowing helpers for DOM event targets used by the Vue renderers.
3
+ *
4
+ * Vue's DOM event handlers receive `Event` whose `target` is typed
5
+ * `EventTarget | null`. The renderers need the concrete `HTMLInputElement`
6
+ * / `HTMLSelectElement` to read `value`, `checked`, or `files`. Plain
7
+ * `event.target as HTMLInputElement` is banned by the project lint
8
+ * rules (`@typescript-eslint/consistent-type-assertions`), so the
9
+ * narrowing happens here through `instanceof` checks.
10
+ *
11
+ * Returning `undefined` when the target is not the expected element
12
+ * type lets each caller decide whether to skip the value-extraction
13
+ * step or treat it as a no-op — both branches are documented at the
14
+ * call site rather than masked behind a sentinel default.
15
+ */
16
+
17
+ /**
18
+ * Narrow an event to its `HTMLInputElement` target, or `undefined`
19
+ * when the event was fired on a different element type.
20
+ */
21
+ export function inputTarget(event: Event): HTMLInputElement | undefined {
22
+ const target = event.target;
23
+ if (target instanceof HTMLInputElement) return target;
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * Narrow an event to its `HTMLSelectElement` target, or `undefined`
29
+ * when the event was fired on a different element type.
30
+ */
31
+ export function selectTarget(event: Event): HTMLSelectElement | undefined {
32
+ const target = event.target;
33
+ if (target instanceof HTMLSelectElement) return target;
34
+ return undefined;
35
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Vue headless renderer — the default {@link VueComponentResolver}
3
+ * implementation.
4
+ *
5
+ * Produces plain Vue {@link VNode}s for every schema field type. Theme
6
+ * adapters replace this by implementing {@link VueComponentResolver}
7
+ * with their own components.
8
+ *
9
+ * Composes the resolver from the individual render functions in
10
+ * `vue/renderers.ts`. Every {@link WalkedField} variant the walker can
11
+ * emit has a registered renderer — missing a registration causes
12
+ * {@link getVueRenderFunction} to return `undefined` and the field to
13
+ * render as nothing.
14
+ */
15
+
16
+ import type { VueComponentResolver } from "./types.ts";
17
+ import {
18
+ renderArray,
19
+ renderBoolean,
20
+ renderConditional,
21
+ renderDiscriminatedUnion,
22
+ renderEnum,
23
+ renderFile,
24
+ renderLiteral,
25
+ renderNegation,
26
+ renderNever,
27
+ renderNull,
28
+ renderNumber,
29
+ renderObject,
30
+ renderRecord,
31
+ renderString,
32
+ renderTuple,
33
+ renderUnion,
34
+ renderUnknown,
35
+ } from "./renderers.ts";
36
+
37
+ /**
38
+ * The Vue headless resolver. Maps every {@link WalkedField} variant to
39
+ * its default render function. Mirrors `react/headless.tsx`'s
40
+ * `headlessResolver` so consumers can switch between adapters without
41
+ * losing field coverage.
42
+ */
43
+ export const headlessVueResolver: VueComponentResolver = {
44
+ string: renderString,
45
+ number: renderNumber,
46
+ boolean: renderBoolean,
47
+ null: renderNull,
48
+ enum: renderEnum,
49
+ object: renderObject,
50
+ record: renderRecord,
51
+ array: renderArray,
52
+ tuple: renderTuple,
53
+ union: renderUnion,
54
+ discriminatedUnion: renderDiscriminatedUnion,
55
+ conditional: renderConditional,
56
+ negation: renderNegation,
57
+ literal: renderLiteral,
58
+ file: renderFile,
59
+ never: renderNever,
60
+ unknown: renderUnknown,
61
+ };