sveltekit-discriminated-fields 0.2.0 → 0.4.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 +184 -84
- package/dist/FieldVariants.svelte +234 -0
- package/dist/FieldVariants.svelte.d.ts +95 -0
- package/dist/discriminated.d.ts +74 -0
- package/dist/discriminated.js +70 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/dist/UnionVariants.svelte +0 -361
- package/dist/UnionVariants.svelte.d.ts +0 -115
- package/dist/discriminated-fields.d.ts +0 -70
- package/dist/discriminated-fields.js +0 -35
- package/dist/infer-discriminator.d.ts +0 -11
- package/dist/infer-discriminator.js +0 -5
|
@@ -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 FieldsObject<K extends string, D, V extends string, AllV extends string> = {
|
|
20
|
+
readonly [P in K]: VariantDiscriminatorField<V, AllV>;
|
|
21
|
+
} & {
|
|
22
|
+
readonly [P in Exclude<keyof D, K>]: NestedField<D[P & keyof D]>;
|
|
23
|
+
};
|
|
24
|
+
type VariantFields<K extends string, D, AllV extends string> = D extends Record<K, infer V> ? {
|
|
25
|
+
readonly type: V;
|
|
26
|
+
readonly fields: FieldsObject<K, D, V & string, AllV>;
|
|
27
|
+
} : never;
|
|
28
|
+
type UndefinedVariant<K extends string, D, AllV extends string> = {
|
|
29
|
+
readonly type: undefined;
|
|
30
|
+
readonly fields: {
|
|
31
|
+
readonly [P in K]: VariantDiscriminatorField<AllV, AllV>;
|
|
32
|
+
} & {
|
|
33
|
+
readonly [P in Exclude<keyof D, K>]: NestedField<D[P & keyof D]>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type SetMethod<K extends string, D> = {
|
|
37
|
+
set: <V extends DiscriminatorValues<K, D>>(data: Extract<D, Record<K, V>>) => void;
|
|
38
|
+
};
|
|
39
|
+
type CommonMethods = RemoteFormFields<unknown> extends {
|
|
40
|
+
allIssues: infer M;
|
|
41
|
+
} ? {
|
|
42
|
+
allIssues: M;
|
|
43
|
+
} : never;
|
|
44
|
+
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;
|
|
45
|
+
/**
|
|
46
|
+
* Wraps discriminated union form fields for type-safe narrowing.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```svelte
|
|
50
|
+
* <script>
|
|
51
|
+
* const priority = $derived(discriminated(priorityForm.fields, "level"));
|
|
52
|
+
* </script>
|
|
53
|
+
*
|
|
54
|
+
* {#if priority.type === "high"}
|
|
55
|
+
* <input {...priority.fields.urgency.as("number")} />
|
|
56
|
+
* {/if}
|
|
57
|
+
*
|
|
58
|
+
* <select {...priority.fields.level.as("select")}>
|
|
59
|
+
* <option {...priority.fields.level.as("option")}>Select...</option>
|
|
60
|
+
* <option {...priority.fields.level.as("option", "high")}>High</option>
|
|
61
|
+
* </select>
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param fields - Form fields from a discriminated union schema
|
|
65
|
+
* @param key - Discriminator key (e.g. 'type', 'kind')
|
|
66
|
+
* @returns Object with `.type` (discriminator value), `.fields` (all form fields), `.set()`, `.allIssues()`
|
|
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
|
+
}>>(fields: T, key: K): DiscriminatedFields<K, DiscriminatedData<T>>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Main function
|
|
3
|
+
// =============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Wraps discriminated union form fields for type-safe narrowing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* const priority = $derived(discriminated(priorityForm.fields, "level"));
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* {#if priority.type === "high"}
|
|
14
|
+
* <input {...priority.fields.urgency.as("number")} />
|
|
15
|
+
* {/if}
|
|
16
|
+
*
|
|
17
|
+
* <select {...priority.fields.level.as("select")}>
|
|
18
|
+
* <option {...priority.fields.level.as("option")}>Select...</option>
|
|
19
|
+
* <option {...priority.fields.level.as("option", "high")}>High</option>
|
|
20
|
+
* </select>
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @param fields - Form fields from a discriminated union schema
|
|
24
|
+
* @param key - Discriminator key (e.g. 'type', 'kind')
|
|
25
|
+
* @returns Object with `.type` (discriminator value), `.fields` (all form fields), `.set()`, `.allIssues()`
|
|
26
|
+
*/
|
|
27
|
+
export function discriminated(fields, key) {
|
|
28
|
+
// Wrap the discriminator field to intercept as("option", value?) calls
|
|
29
|
+
const wrapDiscriminatorField = (field) => {
|
|
30
|
+
return new Proxy(field, {
|
|
31
|
+
get(fieldTarget, fieldProp) {
|
|
32
|
+
if (fieldProp === 'as') {
|
|
33
|
+
return (type, value) => {
|
|
34
|
+
if (type === 'option') {
|
|
35
|
+
return { value: value ?? '' };
|
|
36
|
+
}
|
|
37
|
+
return fieldTarget.as(type, value);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return Reflect.get(fieldTarget, fieldProp);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
// Create the fields proxy that wraps the discriminator field
|
|
45
|
+
const fieldsProxy = new Proxy(fields, {
|
|
46
|
+
get(target, prop) {
|
|
47
|
+
if (prop === key)
|
|
48
|
+
return wrapDiscriminatorField(target[key]);
|
|
49
|
+
return Reflect.get(target, prop);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Create the main proxy with .type, .fields, .set, .allIssues
|
|
53
|
+
const proxy = new Proxy(fields, {
|
|
54
|
+
get(target, prop) {
|
|
55
|
+
if (prop === 'type')
|
|
56
|
+
return target[key].value();
|
|
57
|
+
if (prop === 'fields')
|
|
58
|
+
return fieldsProxy;
|
|
59
|
+
if (prop === 'set')
|
|
60
|
+
return (data) => target.set(data);
|
|
61
|
+
if (prop === 'allIssues')
|
|
62
|
+
return () => target.allIssues?.();
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
has(_target, prop) {
|
|
66
|
+
return prop === 'type' || prop === 'fields' || prop === 'set' || prop === 'allIssues';
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return proxy;
|
|
70
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
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
|
|
2
|
-
export {
|
|
3
|
-
// Re-export
|
|
4
|
-
export { default as
|
|
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,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}
|