sveltekit-discriminated-fields 0.2.0 → 0.3.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.
@@ -0,0 +1,95 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { RemoteFormField, RemoteFormFieldValue, RemoteFormFields } from "@sveltejs/kit";
3
+ type Data<T> = T extends {
4
+ set: (v: infer D) => unknown;
5
+ } ? D : never;
6
+ type Values<K extends string, D> = D extends Record<K, infer V> ? (V extends string ? V : never) : never;
7
+ type NarrowData<K extends string, D, V> = D extends Record<K, V> ? D : never;
8
+ type CommonKeys<D> = keyof D;
9
+ type AllDataKeys<D> = D extends D ? keyof D : never;
10
+ type VariantsWithKey<D, P> = D extends D ? (P extends keyof D ? D : never) : never;
11
+ type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
12
+ type IsPartiallyShared<D, P> = P extends CommonKeys<D> ? false : IsUnion<VariantsWithKey<D, P>>;
13
+ type PartiallySharedKeys<K extends string, D> = {
14
+ [P in Exclude<AllDataKeys<D>, K>]: IsPartiallyShared<D, P> extends true ? P : never;
15
+ }[Exclude<AllDataKeys<D>, K>];
16
+ type TypeAt<D, P extends PropertyKey> = D extends Record<P, infer V> ? V : never;
17
+ type HasMixedTypes<D, P extends PropertyKey> = IsUnion<TypeAt<D, P>>;
18
+ type MixedTypeKeys<K extends string, D, CK extends PropertyKey = Exclude<CommonKeys<D>, K>> = CK extends CK ? HasMixedTypes<D, CK> extends true ? CK : never : never;
19
+ type Validate<K extends string, D, T> = [PartiallySharedKeys<K, D>] extends [never] ? [MixedTypeKeys<K, D>] extends [never] ? T : `Error: Field '${MixedTypeKeys<K, D> & string}' has different types across variants. Common fields must have the same type in all variants.` : `Error: Field '${PartiallySharedKeys<K, D> & string}' exists in some variants but not all. Fields must be either unique to one variant or common to all variants.`;
20
+ type Reserved = "fields" | "key" | "fallback" | "partial" | "css";
21
+ type Field<V> = [V] extends [object] ? RemoteFormFields<V> : RemoteFormField<V & RemoteFormFieldValue>;
22
+ type SnippetFields<K extends string, D, V extends string, Narrow = NarrowData<K, D, V>> = {
23
+ readonly [P in Exclude<keyof Narrow, K | CommonKeys<D>>]: Field<Narrow[P & keyof Narrow]>;
24
+ } & {
25
+ readonly [P in K]: Field<V>;
26
+ };
27
+ export type VariantProps = {
28
+ "data-fv"?: string;
29
+ };
30
+ export type VariantSnippetArg<F> = VariantProps & {
31
+ readonly fields: F;
32
+ };
33
+ type Snippets<K extends string, D, V extends string, IsPartial extends boolean> = IsPartial extends true ? {
34
+ [P in Exclude<V, Reserved>]?: Snippet<[VariantSnippetArg<SnippetFields<K, D, P>>]>;
35
+ } : {
36
+ [P in Exclude<V, Reserved>]: Snippet<[VariantSnippetArg<SnippetFields<K, D, P>>]>;
37
+ };
38
+ export type FieldVariantsProps<K extends string, T extends {
39
+ set: (v: never) => unknown;
40
+ } & Record<K, {
41
+ value(): unknown;
42
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false> = {
43
+ fields: Validate<K, Data<T>, T>;
44
+ key: K;
45
+ /** Optional snippet shown when no variant is selected. Receives (props) for CSS targeting. */
46
+ fallback?: Snippet<[VariantProps]>;
47
+ /** When true, variant snippets are optional (default: false) */
48
+ partial?: IsPartial;
49
+ /** Set to false to disable CSS generation (default: true) */
50
+ css?: boolean;
51
+ } & Snippets<K, Data<T>, V, IsPartial>;
52
+ declare function $$render<K extends string, T extends {
53
+ set: (v: never) => unknown;
54
+ } & Record<K, {
55
+ value(): unknown;
56
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false>(): {
57
+ props: FieldVariantsProps<K, T, V, IsPartial>;
58
+ exports: {};
59
+ bindings: "";
60
+ slots: {};
61
+ events: {};
62
+ };
63
+ declare class __sveltets_Render<K extends string, T extends {
64
+ set: (v: never) => unknown;
65
+ } & Record<K, {
66
+ value(): unknown;
67
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false> {
68
+ props(): ReturnType<typeof $$render<K, T, V, IsPartial>>['props'];
69
+ events(): ReturnType<typeof $$render<K, T, V, IsPartial>>['events'];
70
+ slots(): ReturnType<typeof $$render<K, T, V, IsPartial>>['slots'];
71
+ bindings(): "";
72
+ exports(): {};
73
+ }
74
+ interface $$IsomorphicComponent {
75
+ new <K extends string, T extends {
76
+ set: (v: never) => unknown;
77
+ } & Record<K, {
78
+ value(): unknown;
79
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<K, T, V, IsPartial>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<K, T, V, IsPartial>['props']>, ReturnType<__sveltets_Render<K, T, V, IsPartial>['events']>, ReturnType<__sveltets_Render<K, T, V, IsPartial>['slots']>> & {
80
+ $$bindings?: ReturnType<__sveltets_Render<K, T, V, IsPartial>['bindings']>;
81
+ } & ReturnType<__sveltets_Render<K, T, V, IsPartial>['exports']>;
82
+ <K extends string, T extends {
83
+ set: (v: never) => unknown;
84
+ } & Record<K, {
85
+ value(): unknown;
86
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false>(internal: unknown, props: ReturnType<__sveltets_Render<K, T, V, IsPartial>['props']> & {}): ReturnType<__sveltets_Render<K, T, V, IsPartial>['exports']>;
87
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any, any, any>['bindings']>;
88
+ }
89
+ declare const FieldVariants: $$IsomorphicComponent;
90
+ type FieldVariants<K extends string, T extends {
91
+ set: (v: never) => unknown;
92
+ } & Record<K, {
93
+ value(): unknown;
94
+ }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false> = InstanceType<typeof FieldVariants<K, T, V, IsPartial>>;
95
+ export default FieldVariants;
@@ -0,0 +1,74 @@
1
+ import type { RemoteFormField, RemoteFormFields, RemoteFormFieldType, RemoteFormFieldValue } from '@sveltejs/kit';
2
+ export type DiscriminatedData<T> = T extends {
3
+ set: (v: infer D) => unknown;
4
+ } ? D : never;
5
+ type DiscriminatorValues<K extends string, D> = D extends Record<K, infer V> ? Exclude<V, undefined> : never;
6
+ type RadioProps = RemoteFormField<string> extends {
7
+ as(type: 'radio', value: string): infer R;
8
+ } ? R : never;
9
+ type OptionProps = {
10
+ value: string;
11
+ };
12
+ type VariantDiscriminatorField<V extends string, AllV extends string> = Omit<RemoteFormField<V>, 'as'> & {
13
+ as(type: 'radio', value: AllV): RadioProps;
14
+ as(type: 'option'): OptionProps;
15
+ as(type: 'option', value: AllV): OptionProps;
16
+ as(type: Exclude<RemoteFormFieldType<V>, 'radio'>, ...args: unknown[]): ReturnType<RemoteFormField<V>['as']>;
17
+ };
18
+ type NestedField<V> = [V] extends [object] ? RemoteFormFields<V> : RemoteFormField<V & RemoteFormFieldValue>;
19
+ type VariantFields<K extends string, D, AllV extends string> = D extends Record<K, infer V> ? {
20
+ readonly [P in `${K}Value`]: V;
21
+ } & {
22
+ readonly [P in K]: VariantDiscriminatorField<V & string, AllV>;
23
+ } & {
24
+ readonly [P in Exclude<keyof D, K>]: NestedField<D[P & keyof D]>;
25
+ } : never;
26
+ type UndefinedVariant<K extends string, D, AllV extends string> = {
27
+ readonly [P in `${K}Value`]: undefined;
28
+ } & {
29
+ readonly [P in K]: VariantDiscriminatorField<AllV, AllV>;
30
+ } & {
31
+ readonly [P in Exclude<keyof D, K>]: NestedField<D[P & keyof D]>;
32
+ };
33
+ type SetMethod<K extends string, D> = {
34
+ set: <V extends DiscriminatorValues<K, D>>(data: Extract<D, Record<K, V>>) => void;
35
+ };
36
+ type CommonMethods = RemoteFormFields<unknown> extends {
37
+ allIssues: infer M;
38
+ } ? {
39
+ allIssues: M;
40
+ } : never;
41
+ type DiscriminatedFields<K extends string, D, AllV extends string = DiscriminatorValues<K, D> & string> = (VariantFields<K, D, AllV> | UndefinedVariant<K, D, AllV>) & SetMethod<K, D> & CommonMethods;
42
+ /**
43
+ * Marks discriminated union form fields for type-safe narrowing.
44
+ * - All original fields pass through unchanged (type, issues, allIssues, etc.)
45
+ * - `set` is overridden with type-safe version
46
+ * - `${key}Value` is added for discriminator value (e.g., `reward.typeValue`)
47
+ * - Discriminator field `.as("radio", value)` is type-safe (only valid values allowed)
48
+ * - Discriminator field `.as("option", value?)` is type-safe for select options
49
+ *
50
+ * @example
51
+ * ```svelte
52
+ * <script>
53
+ * const priority = $derived(discriminated("level", priorityForm.fields));
54
+ * </script>
55
+ *
56
+ * <input {...priority.level.as("radio", "high")} /> <!-- type-safe: only valid values allowed -->
57
+ *
58
+ * <select {...priority.level.as("select")}>
59
+ * <option {...priority.level.as("option")}>Select...</option>
60
+ * <option {...priority.level.as("option", "high")}>High</option>
61
+ * </select>
62
+ * ```
63
+ *
64
+ * @param key - Discriminator key (e.g. 'type')
65
+ * @param fields - Form fields from a discriminated union schema
66
+ * @returns Passthrough object with type-safe set(), ${key}Value, .as("radio", value), and .as("option", value?)
67
+ */
68
+ export declare function discriminated<K extends string, T extends {
69
+ set: (v: never) => unknown;
70
+ } & Record<K, {
71
+ value(): unknown;
72
+ as(type: 'radio', value: string): object;
73
+ }>>(key: K, fields: T): DiscriminatedFields<K, DiscriminatedData<T>>;
74
+ export {};
@@ -0,0 +1,62 @@
1
+ // =============================================================================
2
+ // Main function
3
+ // =============================================================================
4
+ /**
5
+ * Marks discriminated union form fields for type-safe narrowing.
6
+ * - All original fields pass through unchanged (type, issues, allIssues, etc.)
7
+ * - `set` is overridden with type-safe version
8
+ * - `${key}Value` is added for discriminator value (e.g., `reward.typeValue`)
9
+ * - Discriminator field `.as("radio", value)` is type-safe (only valid values allowed)
10
+ * - Discriminator field `.as("option", value?)` is type-safe for select options
11
+ *
12
+ * @example
13
+ * ```svelte
14
+ * <script>
15
+ * const priority = $derived(discriminated("level", priorityForm.fields));
16
+ * </script>
17
+ *
18
+ * <input {...priority.level.as("radio", "high")} /> <!-- type-safe: only valid values allowed -->
19
+ *
20
+ * <select {...priority.level.as("select")}>
21
+ * <option {...priority.level.as("option")}>Select...</option>
22
+ * <option {...priority.level.as("option", "high")}>High</option>
23
+ * </select>
24
+ * ```
25
+ *
26
+ * @param key - Discriminator key (e.g. 'type')
27
+ * @param fields - Form fields from a discriminated union schema
28
+ * @returns Passthrough object with type-safe set(), ${key}Value, .as("radio", value), and .as("option", value?)
29
+ */
30
+ export function discriminated(key, fields) {
31
+ // Wrap the discriminator field to intercept as("option", value?) calls
32
+ const wrapDiscriminatorField = (field) => {
33
+ return new Proxy(field, {
34
+ get(fieldTarget, fieldProp) {
35
+ if (fieldProp === 'as') {
36
+ return (type, value) => {
37
+ if (type === 'option') {
38
+ return { value: value ?? '' };
39
+ }
40
+ return fieldTarget.as(type, value);
41
+ };
42
+ }
43
+ return Reflect.get(fieldTarget, fieldProp);
44
+ }
45
+ });
46
+ };
47
+ const proxy = new Proxy(fields, {
48
+ get(target, prop) {
49
+ if (prop === `${key}Value`)
50
+ return target[key].value();
51
+ if (prop === 'set')
52
+ return (data) => target.set(data);
53
+ if (prop === key)
54
+ return wrapDiscriminatorField(target[key]);
55
+ return Reflect.get(target, prop);
56
+ },
57
+ has(target, prop) {
58
+ return prop === `${key}Value` || prop in target;
59
+ }
60
+ });
61
+ return proxy;
62
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { discriminatedFields, type DiscriminatedData } from './discriminated-fields.js';
2
- export type { InferDiscriminator } from './infer-discriminator.js';
3
- export { default as UnionVariants } from './UnionVariants.svelte';
4
- export type { UnionVariantsProps } from './UnionVariants.svelte';
1
+ export { discriminated, type DiscriminatedData } from './discriminated.js';
2
+ export { default as FieldVariants } from './FieldVariants.svelte';
3
+ export type { FieldVariantsProps, VariantProps, VariantSnippetArg } from './FieldVariants.svelte';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Re-export discriminatedFields function and types
2
- export { discriminatedFields } from './discriminated-fields.js';
3
- // Re-export UnionVariants component and its types
4
- export { default as UnionVariants } from './UnionVariants.svelte';
1
+ // Re-export discriminated function and types
2
+ export { discriminated } from './discriminated.js';
3
+ // Re-export FieldVariants component and its types
4
+ export { default as FieldVariants } from './FieldVariants.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltekit-discriminated-fields",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Type-safe discriminated union support for SvelteKit form fields",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -1,361 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
- import type { InferDiscriminator } from "./infer-discriminator.js";
4
-
5
- // Extract discriminator values from a union of field objects
6
- type DiscriminatorValues<K extends string, T> =
7
- T extends Record<K, { value(): infer V }>
8
- ? V extends string
9
- ? V
10
- : never
11
- : never;
12
-
13
- // Narrow T to the variant where discriminator equals V
14
- type NarrowToVariant<K extends string, T, V> =
15
- T extends Record<K, { value(): V }> ? T : never;
16
-
17
- // Reserved prop names that can't be variant names
18
- type ReservedProps = "fields" | "key" | "fallback" | "partial";
19
-
20
- // =============================================================================
21
- // Shared field name detection
22
- // Fields can be:
23
- // - Unique to one variant: OK
24
- // - Common to ALL variants: OK (accessible at top level, outside UnionVariants)
25
- // - Shared by SOME but not ALL: ERROR (would cause duplicate form submissions)
26
- // =============================================================================
27
-
28
- // Keys that look like form fields (have a value() method) - excludes methods like set/allIssues
29
- type FieldKeys<T> = {
30
- [P in keyof T]: T[P] extends { value(): unknown } ? P : never;
31
- }[keyof T];
32
-
33
- // Get field keys for a specific variant (excluding discriminator)
34
- type VariantFieldKeys<K extends string, T, V extends string> = Exclude<
35
- FieldKeys<NarrowToVariant<K, T, V>>,
36
- K
37
- >;
38
-
39
- // Get union of field keys from all variants EXCEPT V (distributes over the Exclude result)
40
- type OtherVariantsFieldKeys<
41
- K extends string,
42
- T,
43
- V extends string,
44
- OtherV extends string = Exclude<DiscriminatorValues<K, T>, V>,
45
- > = OtherV extends OtherV ? VariantFieldKeys<K, T, OtherV> : never;
46
-
47
- // Keys that variant V shares with at least one other variant
48
- type SharedFieldKeys<
49
- K extends string,
50
- T,
51
- V extends string,
52
- > = VariantFieldKeys<K, T, V> & OtherVariantsFieldKeys<K, T, V>;
53
-
54
- // Union of all shared keys across all variants (distributes over V)
55
- type AllSharedFieldKeys<
56
- K extends string,
57
- T,
58
- V extends string = DiscriminatorValues<K, T>,
59
- > = V extends V ? SharedFieldKeys<K, T, V> : never;
60
-
61
- // Check if field P exists in variant V
62
- type HasField<T, P> = P extends keyof T
63
- ? T[P] extends { value(): unknown }
64
- ? true
65
- : false
66
- : false;
67
-
68
- // Check if field P exists in ALL variants (distributes, so result is union of true/false)
69
- type FieldExistsInAll<
70
- K extends string,
71
- T,
72
- P extends string,
73
- V extends string = DiscriminatorValues<K, T>,
74
- > = V extends V ? HasField<NarrowToVariant<K, T, V>, P> : never;
75
-
76
- // A field is common if it exists in all variants (no false in the result)
77
- type IsCommonField<K extends string, T, P extends string> = false extends FieldExistsInAll<K, T, P>
78
- ? false
79
- : true;
80
-
81
- // =============================================================================
82
- // Type uniformity check - fields shared across variants must have the same type
83
- // =============================================================================
84
-
85
- // Get the value type of field P in variant V
86
- type FieldValueType<K extends string, T, V extends string, P extends string> = NarrowToVariant<
87
- K,
88
- T,
89
- V
90
- >[P & keyof NarrowToVariant<K, T, V>] extends { value(): infer R }
91
- ? R
92
- : never;
93
-
94
- // Union of all value types for field P across all variants
95
- type AllFieldValueTypes<
96
- K extends string,
97
- T,
98
- P extends string,
99
- V extends string = DiscriminatorValues<K, T>,
100
- > = V extends V ? FieldValueType<K, T, V, P> : never;
101
-
102
- // Check if the union of all types equals each individual type (i.e., all types are the same)
103
- // If types differ: (string | number) extends string = false
104
- // If types same: string extends string = true
105
- type FieldTypeMatchesInVariant<
106
- K extends string,
107
- T,
108
- P extends string,
109
- V extends string,
110
- Union = AllFieldValueTypes<K, T, P>,
111
- > = [Union] extends [FieldValueType<K, T, V, P>] ? true : false;
112
-
113
- // Check if field P has the same type in ALL variants
114
- type FieldHasSameTypeInAll<
115
- K extends string,
116
- T,
117
- P extends string,
118
- V extends string = DiscriminatorValues<K, T>,
119
- > = V extends V ? FieldTypeMatchesInVariant<K, T, P, V> : never;
120
-
121
- // A field has uniform type if no variant returns false
122
- type IsUniformTypeField<K extends string, T, P extends string> = false extends FieldHasSameTypeInAll<
123
- K,
124
- T,
125
- P
126
- >
127
- ? false
128
- : true;
129
-
130
- // =============================================================================
131
- // Field classification
132
- // =============================================================================
133
-
134
- // Fields shared by SOME but not ALL variants (problematic - would cause duplicate submissions)
135
- type PartiallySharedFieldKeys<K extends string, T> = {
136
- [P in AllSharedFieldKeys<K, T>]: IsCommonField<K, T, P & string> extends true ? never : P;
137
- }[AllSharedFieldKeys<K, T>];
138
-
139
- // Fields in ALL variants but with DIFFERENT types (problematic - type mismatch)
140
- type MixedTypeFieldKeys<K extends string, T> = {
141
- [P in AllSharedFieldKeys<K, T>]: IsCommonField<K, T, P & string> extends true
142
- ? IsUniformTypeField<K, T, P & string> extends true
143
- ? never
144
- : P
145
- : never;
146
- }[AllSharedFieldKeys<K, T>];
147
-
148
- // Fields common to ALL variants WITH the same type (safe to use outside UnionVariants)
149
- type CommonFieldKeys<K extends string, T> = {
150
- [P in AllSharedFieldKeys<K, T>]: IsCommonField<K, T, P & string> extends true
151
- ? IsUniformTypeField<K, T, P & string> extends true
152
- ? P
153
- : never
154
- : never;
155
- }[AllSharedFieldKeys<K, T>];
156
-
157
- // =============================================================================
158
- // Validation - produces error messages for invalid field configurations
159
- // =============================================================================
160
-
161
- // Validates that T has no partially shared field keys
162
- type ValidateNoPartiallySharedFields<K extends string, T> = [
163
- PartiallySharedFieldKeys<K, T>,
164
- ] extends [never]
165
- ? T
166
- : `Error: Field '${PartiallySharedFieldKeys<K, T> & string}' exists in some variants but not all. Fields must be either unique to one variant or common to all variants.`;
167
-
168
- // Validates that T has no mixed-type field keys
169
- type ValidateNoMixedTypeFields<K extends string, T> = [MixedTypeFieldKeys<K, T>] extends [never]
170
- ? T
171
- : `Error: Field '${MixedTypeFieldKeys<K, T> & string}' exists in all variants but with different types. Shared fields must have the same type across all variants.`;
172
-
173
- // Combined validation
174
- type ValidateFields<K extends string, T> = [PartiallySharedFieldKeys<K, T>] extends [never]
175
- ? [MixedTypeFieldKeys<K, T>] extends [never]
176
- ? T
177
- : ValidateNoMixedTypeFields<K, T>
178
- : ValidateNoPartiallySharedFields<K, T>;
179
-
180
- // Variant type with common fields removed (for snippet parameters)
181
- // This prevents accidentally rendering common fields twice
182
- type VariantOnlyFields<K extends string, T, V extends string> = Omit<
183
- NarrowToVariant<K, T, V>,
184
- CommonFieldKeys<K, T>
185
- >;
186
-
187
- // Snippet props mapped from variant values - optionality controlled by Partial flag
188
- // Note: snippets receive variant-only fields (common fields are excluded to prevent duplicates)
189
- type VariantSnippets<
190
- K extends string,
191
- T,
192
- V extends string,
193
- IsPartial extends boolean,
194
- > = IsPartial extends true
195
- ? { [P in Exclude<V, ReservedProps>]?: Snippet<[VariantOnlyFields<K, T, P>]> }
196
- : { [P in Exclude<V, ReservedProps>]: Snippet<[VariantOnlyFields<K, T, P>]> };
197
-
198
- // Full props type with explicit key
199
- export type UnionVariantsPropsWithKey<
200
- K extends string,
201
- T extends Record<K, { value(): unknown }>,
202
- V extends string = DiscriminatorValues<K, T>,
203
- IsPartial extends boolean = false,
204
- > = {
205
- fields: ValidateFields<K, T>;
206
- key: K;
207
- /** Optional snippet shown when no variant is selected */
208
- fallback?: Snippet;
209
- /** When true, variant snippets are optional (default: false) */
210
- partial?: IsPartial;
211
- /**
212
- * CSS selector for the discriminator input element (select or radio container).
213
- * When provided, uses this selector instead of the default name-based lookup.
214
- * Example: "#method-select" for a select, or "#radio-group" for radios
215
- */
216
- selector?: string;
217
- } & VariantSnippets<K, T, V, IsPartial>;
218
-
219
- // Props type with auto-inferred key (key is optional)
220
- export type UnionVariantsPropsInferred<
221
- T extends Record<string, { value(): unknown }>,
222
- K extends string = InferDiscriminator<T> extends infer I ? (I extends string ? I : never) : never,
223
- V extends string = DiscriminatorValues<K, T>,
224
- IsPartial extends boolean = false,
225
- > = InferDiscriminator<T> extends string
226
- ? {
227
- fields: ValidateFields<K, T>;
228
- key?: K;
229
- fallback?: Snippet;
230
- partial?: IsPartial;
231
- selector?: string;
232
- } & VariantSnippets<K, T, V, IsPartial>
233
- : {
234
- fields: InferDiscriminator<T>; // This will be the error message
235
- key: never;
236
- };
237
-
238
- // Combined props type - key can be explicit or inferred
239
- export type UnionVariantsProps<
240
- K extends string,
241
- T extends Record<K, { value(): unknown }>,
242
- V extends string = DiscriminatorValues<K, T>,
243
- IsPartial extends boolean = false,
244
- > = UnionVariantsPropsWithKey<K, T, V, IsPartial>;
245
- </script>
246
-
247
- <script
248
- lang="ts"
249
- generics="K extends string, T extends Record<K, { value(): unknown }>, V extends string = DiscriminatorValues<K, T>, IsPartial extends boolean = false"
250
- >
251
- type Props = UnionVariantsProps<K, T, V, IsPartial>;
252
-
253
- let { fields, key: keyProp, fallback, partial, selector, ...snippets }: Props = $props();
254
-
255
- // Auto-detect discriminator key if not provided
256
- function findDiscriminatorKey(f: Record<string, { value(): unknown }>): string {
257
- const fieldKeys = Object.keys(f).filter((k) => {
258
- const field = f[k];
259
- return field && typeof field === "object" && "value" in field && typeof field.value === "function";
260
- });
261
-
262
- const candidates = fieldKeys.filter((k) => {
263
- const field = f[k];
264
- const value = field.value();
265
- return typeof value === "string" || value === undefined;
266
- });
267
-
268
- if (candidates.length === 0) {
269
- throw new Error("No valid discriminator key found in fields");
270
- }
271
- return candidates[0];
272
- }
273
-
274
- // Use provided key or auto-detect
275
- const key = $derived(keyProp ?? findDiscriminatorKey(fields as Record<string, { value(): unknown }>)) as K;
276
-
277
- // Get all variant values from the snippet names (excluding fallback)
278
- const variantValues = Object.keys(snippets) as V[];
279
-
280
- // Get the actual field name from the discriminator field (handles nested paths like "shipping.method")
281
- const fieldName = $derived.by(() => {
282
- const discriminatorField = (fields as Record<K, { as(type: "select"): { name: string } }>)[key];
283
- return discriminatorField?.as("select")?.name ?? key;
284
- });
285
-
286
- // Generate CSS for showing/hiding variants based on select or radio value
287
- const css = $derived.by(() => {
288
- const attr = `data-union-${key}`;
289
- const name = fieldName;
290
-
291
- if (selector) {
292
- // Input-based: use :has() with the provided selector to find any ancestor
293
- // Supports: select elements, radio inputs directly, or radio containers
294
- const selOpt = (v: string) => `*:has(${selector} option[value="${v}"]:checked)`;
295
- const radDirect = (v: string) => `*:has(${selector}[value="${v}"]:checked)`;
296
- const radContainer = (v: string) => `*:has(${selector} input[value="${v}"]:checked)`;
297
-
298
- // Hide all variant sections by default
299
- const hideAll = `*:has(${selector}) [${attr}]:not([${attr}="fallback"]) { display: none; }\n`;
300
-
301
- // Show the variant section that matches the selected value
302
- const showVariants = variantValues
303
- .map((v) => `${selOpt(v)} [${attr}="${v}"],
304
- ${radDirect(v)} [${attr}="${v}"],
305
- ${radContainer(v)} [${attr}="${v}"] { display: contents; }`)
306
- .join("\n");
307
-
308
- // Hide fallback when any variant is selected
309
- const hideFallback =
310
- fallback && variantValues.length
311
- ? `*:has(${selector}:checked) [${attr}="fallback"],
312
- *:has(${selector} input:checked) [${attr}="fallback"],
313
- ${variantValues.map((v) => `${selOpt(v)} [${attr}="fallback"]`).join(",\n")} { display: none; }\n`
314
- : "";
315
-
316
- return hideAll + showVariants + hideFallback;
317
- }
318
-
319
- // Sibling-based (default): look for sibling elements containing the discriminator
320
- const sel = (s: string) => `*:has(select[name="${name}"]${s}) ~`;
321
- const rad = (s: string) => `*:has(input[name="${name}"]${s}) ~`;
322
-
323
- // Hide all variant sections by default (but not fallback)
324
- const hideAll = `${sel("")} [${attr}]:not([${attr}="fallback"]),
325
- ${rad("")} [${attr}]:not([${attr}="fallback"]) { display: none; }\n`;
326
-
327
- // Show the variant section that matches the selected value
328
- const showVariants = variantValues
329
- .map((v) => `${sel(` option[value="${v}"]:checked`)} [${attr}="${v}"],
330
- ${rad(`[value="${v}"]:checked`)} [${attr}="${v}"] { display: contents; }`)
331
- .join("\n");
332
-
333
- // Hide fallback when any variant is selected
334
- const hideFallback =
335
- fallback && variantValues.length
336
- ? `${rad(":checked")} [${attr}="fallback"],
337
- ${variantValues.map((v) => `${sel(` option[value="${v}"]:checked`)} [${attr}="fallback"]`).join(",\n")} { display: none; }\n`
338
- : "";
339
-
340
- return hideAll + showVariants + hideFallback;
341
- });
342
- </script>
343
-
344
- <svelte:head>
345
- {@html `<style>${css}</style>`}
346
- </svelte:head>
347
-
348
- {#if fallback}
349
- {@const attrs = { [`data-union-${key}`]: "fallback" }}
350
- <div {...attrs}>
351
- {@render fallback()}
352
- </div>
353
- {/if}
354
-
355
- {#each variantValues as v (v)}
356
- {@const snippet = (snippets as unknown as Record<V, Snippet<[T]>>)[v]}
357
- {@const attrs = { [`data-union-${key}`]: v }}
358
- <div {...attrs}>
359
- {@render snippet(fields as T)}
360
- </div>
361
- {/each}