sveltekit-discriminated-fields 0.1.2 → 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.
package/README.md CHANGED
@@ -2,16 +2,36 @@
2
2
 
3
3
  Type-safe discriminated union support for SvelteKit remote function form fields.
4
4
 
5
- ## The Problem
5
+ This library provides two complementary tools for working with discriminated unions in SvelteKit forms:
6
6
 
7
- When using SvelteKit's remote function `form()` with discriminated union schemas (e.g., Zod's `z.discriminatedUnion`), the generated `fields` object is a **union of field objects**. TypeScript only allows access to properties that exist on ALL variants - meaning variant-specific fields are completely inaccessible.
7
+ - **`discriminated()`** - A wrapper function that enables proper TypeScript type narrowing on form field objects
8
+ - **`FieldVariants`** - A component that renders variant-specific form sections with CSS-only visibility, supporting progressive enhancement (works without JavaScript)
8
9
 
9
- Given a typical SvelteKit setup:
10
+ The implementation prioritises:
11
+
12
+ - **Static type safety** - Catch errors at compile time with clear error messages
13
+ - **Progressive enhancement** - Forms work identically with or without JavaScript
14
+ - **Minimal boilerplate** - Simple API that stays out of your way
15
+ - **Flexibility** - Use as much or as little as you need
16
+
17
+ This library exposes existing SvelteKit form behaviour with improved typing for discriminated unions. Runtime overhead is minimal - just a thin proxy wrapper.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install sveltekit-discriminated-fields
23
+ ```
24
+
25
+ ## FieldVariants Component
26
+
27
+ The `FieldVariants` component provides declarative variant rendering with **full progressive enhancement support**. It uses CSS-only visibility toggling via `form:has()` selectors, so forms work identically with or without JavaScript enabled.
28
+
29
+ Given a SvelteKit remote function using Zod (this library also works with [Valibot](./test/src/routes/valibot-form) or other validation libraries):
10
30
 
11
31
  ```typescript
12
32
  // data.remote.ts
13
33
  import { z } from "zod";
14
- import { form } from "$app/server/remote";
34
+ import { form } from "$app/server";
15
35
 
16
36
  const shapeSchema = z.discriminatedUnion("kind", [
17
37
  z.object({ kind: z.literal("circle"), radius: z.number() }),
@@ -24,152 +44,327 @@ const shapeSchema = z.discriminatedUnion("kind", [
24
44
  ]);
25
45
 
26
46
  export const shapeForm = form(shapeSchema, async (data) => {
27
- // handle form submission
47
+ // handle submission
28
48
  });
29
49
  ```
30
50
 
51
+ Use `FieldVariants` to render variant-specific fields:
52
+
31
53
  ```svelte
32
- <!-- +page.svelte (without this library) -->
33
54
  <script lang="ts">
34
- import { shapeForm } from './data.remote.ts';
55
+ import { shapeForm } from './data.remote';
56
+ import { FieldVariants } from 'sveltekit-discriminated-fields';
57
+ </script>
58
+
59
+ <form {...shapeForm}>
60
+ <select {...shapeForm.fields.kind.as('select')}>
61
+ <option value="">Select a shape...</option>
62
+ <option value="circle">Circle</option>
63
+ <option value="rectangle">Rectangle</option>
64
+ <option value="point">Point</option>
65
+ </select>
66
+
67
+ <FieldVariants fields={shapeForm.fields} key="kind">
68
+ {#snippet fallback(props)}
69
+ <p {...props}>Please select a shape type above.</p>
70
+ {/snippet}
71
+
72
+ {#snippet circle(v)}
73
+ <label {...v}>
74
+ Radius: <input {...v.fields.radius.as('number')} />
75
+ </label>
76
+ {/snippet}
77
+
78
+ {#snippet rectangle(v)}
79
+ <div {...v}>
80
+ <input {...v.fields.width.as('number')} placeholder="Width" />
81
+ <input {...v.fields.height.as('number')} placeholder="Height" />
82
+ </div>
83
+ {/snippet}
84
+
85
+ {#snippet point(v)}
86
+ <p {...v}>Point has no additional fields.</p>
87
+ {/snippet}
88
+ </FieldVariants>
89
+
90
+ <button type="submit">Submit</button>
91
+ </form>
92
+ ```
93
+
94
+ ### Snippet Parameters
95
+
96
+ Each variant snippet receives a single argument that mirrors how forms work in SvelteKit:
97
+
98
+ - **Spread for CSS targeting**: `{...v}` - Adds the `data-fv` attribute for CSS visibility
99
+ - **Access fields**: `v.fields.radius` - Provides the variant-specific form fields
100
+
101
+ This pattern is consistent with how you use forms: `<form {...form}>` + `form.fields.x`.
102
+
103
+ ### Snippets and Fields
104
+
105
+ Each snippet receives correctly narrowed fields for that variant - TypeScript knows `v.fields.radius` exists in the `circle` snippet but not in `rectangle`. Only valid discriminator values are accepted.
35
106
 
36
- const form = shapeForm();
37
- const shape = form.fields;
107
+ Snippets only receive fields **specific to that variant**. Fields common to all variants (same name and type) should be rendered outside `FieldVariants` to prevent accidental duplicate inputs. Fields shared by some but not all variants, or with differing types across variants, produce compile-time errors.
108
+
109
+ ### Radio Buttons
110
+
111
+ For radio button discriminators, you must use the `discriminated()` wrapper. The raw SvelteKit field object's `.as("radio", value)` method doesn't work with discriminated unions (causes a static error). The wrapped version is type-safe - only valid discriminator values are accepted:
112
+
113
+ ```svelte
114
+ <script lang="ts">
115
+ import { shapeForm } from './data.remote';
116
+ import { discriminated, FieldVariants } from 'sveltekit-discriminated-fields';
117
+
118
+ const shape = $derived(discriminated('kind', shapeForm.fields));
38
119
  </script>
39
120
 
40
- <select {...shape.kind.as('select')}>
41
- <option value="circle">Circle</option>
42
- <option value="rectangle">Rectangle</option>
121
+ <form {...shapeForm}>
122
+ <fieldset>
123
+ <label><input {...shape.kind.as("radio", "circle")} /> Circle</label>
124
+ <label><input {...shape.kind.as("radio", "rectangle")} /> Rectangle</label>
125
+ <label><input {...shape.kind.as("radio", "point")} /> Point</label>
126
+ </fieldset>
127
+
128
+ <FieldVariants fields={shape} key="kind">
129
+ {#snippet fallback(props)}
130
+ <p {...props}>Select a shape type</p>
131
+ {/snippet}
132
+
133
+ {#snippet circle(v)}
134
+ <label {...v}>
135
+ Radius: <input {...v.fields.radius.as('number')} />
136
+ </label>
137
+ {/snippet}
138
+
139
+ <!-- other snippets -->
140
+ </FieldVariants>
141
+ </form>
142
+ ```
143
+
144
+ See the [radio-form example](./test/src/routes/radio-form) for a complete working example.
145
+
146
+ ### Select Options
147
+
148
+ For select elements, you can use `.as("option", value)` for type-safe option values. This is optional - you can still use `value="..."` directly if you prefer:
149
+
150
+ ```svelte
151
+ <select {...shape.kind.as("select")}>
152
+ <!-- Type-safe: typos caught at compile time -->
153
+ <option {...shape.kind.as("option")}>Select a shape...</option>
154
+ <option {...shape.kind.as("option", "circle")}>Circle</option>
155
+ <option {...shape.kind.as("option", "rectangle")}>Rectangle</option>
156
+
157
+ <!-- Also works: standard HTML (no type checking) -->
43
158
  <option value="point">Point</option>
44
159
  </select>
160
+ ```
45
161
 
46
- {#if shape.kind.value() === 'circle'}
47
- <!-- ERROR: Property 'radius' does not exist on type 'ShapeFields' -->
48
- <input {...shape.radius.as('number')} />
49
- {:else if shape.kind.value() === 'rectangle'}
50
- <!-- ERROR: Property 'width' does not exist on type 'ShapeFields' -->
51
- <input {...shape.width.as('number')} />
52
- {/if}
162
+ - `as("option")` returns `{ value: "" }` for the placeholder option
163
+ - `as("option", "circle")` returns `{ value: "circle" }` with type checking
164
+
165
+ ### CSS-Based Visibility
166
+
167
+ `FieldVariants` uses `form:has()` CSS selectors to show/hide variant sections based on the selected discriminator value. This works automatically for any layout - the discriminator input and variant sections can be anywhere within the same `<form>`.
168
+
169
+ ```svelte
170
+ <form {...shapeForm}>
171
+ <div class="header">
172
+ <select {...shapeForm.fields.kind.as('select')}>
173
+ <!-- options -->
174
+ </select>
175
+ </div>
176
+
177
+ <div class="body">
178
+ <!-- Works regardless of DOM structure -->
179
+ <FieldVariants fields={shapeForm.fields} key="kind">
180
+ <!-- snippets -->
181
+ </FieldVariants>
182
+ </div>
183
+ </form>
53
184
  ```
54
185
 
55
- The issue: `shape.kind.value() === 'circle'` doesn't narrow the type because method calls don't provide TypeScript control flow analysis. You're stuck - variant-specific fields are inaccessible, and there's no way to narrow to access them.
186
+ See the [selector-form example](./test/src/routes/selector-form) for select elements or [selector-radio-form example](./test/src/routes/selector-radio-form) for radio buttons.
56
187
 
57
- ## The Solution
188
+ ### Nested and Multiple Unions
58
189
 
59
- This library wraps your discriminated union fields to:
190
+ The discriminated union doesn't need to be the top-level schema. It can be nested within a larger object:
60
191
 
61
- 1. Make all variant fields accessible
62
- 2. Add a `${key}Value` property that enables TypeScript narrowing
192
+ ```typescript
193
+ const orderSchema = z.object({
194
+ orderId: z.string(),
195
+ shipping: z.discriminatedUnion("method", [
196
+ z.object({ method: z.literal("pickup"), store: z.string() }),
197
+ z.object({ method: z.literal("delivery"), address: z.string() }),
198
+ ]),
199
+ });
200
+ ```
63
201
 
64
202
  ```svelte
65
- <!-- +page.svelte (with this library) -->
66
203
  <script lang="ts">
67
- import { shapeForm } from './data.remote.ts';
68
- import { discriminatedFields } from 'sveltekit-discriminated-fields'; // +
204
+ import { discriminated, FieldVariants } from 'sveltekit-discriminated-fields';
69
205
 
70
- const form = shapeForm();
71
- const shape = discriminatedFields('kind', form.fields); // changed
206
+ const shipping = $derived(discriminated('method', orderForm.fields.shipping));
72
207
  </script>
73
208
 
74
- <select {...shape.kind.as('select')}>
75
- <option value="circle">Circle</option>
76
- <option value="rectangle">Rectangle</option>
77
- <option value="point">Point</option>
78
- </select>
209
+ <FieldVariants fields={shipping} key="method">
210
+ {#snippet pickup(v)}
211
+ <input {...v} {...v.fields.store.as('text')} />
212
+ {/snippet}
79
213
 
80
- {#if shape.kindValue === 'circle'}<!-- changed -->
81
- <input {...shape.radius.as('number')} /><!-- now works! -->
82
- {:else if shape.kindValue === 'rectangle'}<!-- changed -->
83
- <input {...shape.width.as('number')} /><!-- now works! -->
84
- <input {...shape.height.as('number')} />
85
- <!-- ERROR: Property 'radius' does not exist - correctly rejected -->
86
- <input {...shape.radius.as('number')} />
87
- {/if}
214
+ {#snippet delivery(v)}
215
+ <input {...v} {...v.fields.address.as('text')} />
216
+ {/snippet}
217
+ </FieldVariants>
88
218
  ```
89
219
 
90
- The changes:
220
+ You can also have multiple discriminated unions in the same form, or even a discriminated union nested within another discriminated union. See the [nested-form example](./test/src/routes/nested-form) for nested unions within objects, or [nested-union-form example](./test/src/routes/nested-union-form) for a union inside a union.
91
221
 
92
- 1. Import `discriminatedFields` from this library
93
- 2. Wrap `form.fields` with `discriminatedFields('kind', ...)`
94
- 3. Use `shape.kindValue` instead of `shape.kind.value()` for narrowing
222
+ ### Partial Variants
95
223
 
96
- ## Installation
224
+ By default, `FieldVariants` requires a snippet for every variant - a compile error appears if one is missing, helping you avoid omissions. When you intentionally want to handle only some variants, use `partial={true}`:
97
225
 
98
- ```bash
99
- npm install sveltekit-discriminated-fields
226
+ ```svelte
227
+ <FieldVariants fields={shape} key="kind" partial={true}>
228
+ {#snippet circle(v)}
229
+ <input {...v} {...v.fields.radius.as('number')} />
230
+ {/snippet}
231
+
232
+ {#snippet rectangle(v)}
233
+ <input {...v} {...v.fields.width.as('number')} />
234
+ {/snippet}
235
+
236
+ <!-- point snippet omitted - nothing shown when point selected -->
237
+ </FieldVariants>
100
238
  ```
101
239
 
102
- ## Usage
240
+ ### Progressive Enhancement
241
+
242
+ `FieldVariants` provides true progressive enhancement:
243
+
244
+ 1. **Before JavaScript loads**: All variant snippets are rendered, CSS handles visibility
245
+ 2. **After JavaScript hydrates**: Switches to conditional rendering, enabling Svelte transitions
246
+
247
+ This means forms work without JavaScript, but once JS loads, you get full Svelte features:
103
248
 
104
249
  ```svelte
105
- <script lang="ts">
106
- import { paymentForm } from './data.remote.ts';
107
- import { discriminatedFields } from 'sveltekit-discriminated-fields';
250
+ <FieldVariants fields={shape} key="kind">
251
+ {#snippet circle(v)}
252
+ <!-- Svelte transitions work after hydration -->
253
+ <div {...v} transition:slide>
254
+ <input {...v.fields.radius.as('number')} />
255
+ </div>
256
+ {/snippet}
257
+ </FieldVariants>
258
+ ```
108
259
 
109
- const form = paymentForm();
260
+ ### Disabling CSS
110
261
 
111
- // Wrap fields with $derived for reactivity
112
- // The first argument is your discriminator key (e.g., 'type', 'kind', 'channel')
113
- const payment = $derived(discriminatedFields('type', form.fields));
262
+ If you want to handle visibility yourself, disable CSS generation:
114
263
 
115
- function setCardDefaults() {
116
- // set() is type-safe - TypeScript enforces correct fields for each variant
117
- payment.set({ type: 'card', cardNumber: '', cvv: '' });
118
- }
119
- </script>
264
+ ```svelte
265
+ <FieldVariants fields={shape} key="kind" css={false}>
266
+ <!-- snippets -->
267
+ </FieldVariants>
268
+ ```
120
269
 
121
- <!-- typeValue is undefined until form is initialized -->
122
- {#if payment.typeValue === undefined}
123
- <p>Loading...</p>
124
- {:else}
125
- <select {...payment.type.as('select')}>
126
- <option value="card">Card</option>
127
- <option value="bank">Bank Transfer</option>
128
- </select>
270
+ ## discriminated Function
271
+
272
+ When using SvelteKit's remote function `form()` with discriminated union schemas, the generated `fields` object is a union of field objects. TypeScript only allows access to properties that exist on ALL variants - meaning variant-specific fields are inaccessible, and `.as("radio", value)` doesn't work.
273
+
274
+ The `discriminated()` function wraps your form fields to:
129
275
 
130
- <!-- Use typeValue (not type.value()) for type narrowing -->
131
- {#if payment.typeValue === 'card'}
132
- <input {...payment.cardNumber.as('string')} />
133
- <input {...payment.cvv.as('string')} />
134
- {:else if payment.typeValue === 'bank'}
135
- <input {...payment.accountNumber.as('string')} />
136
- <input {...payment.sortCode.as('string')} />
137
- {/if}
276
+ 1. Make all variant fields accessible
277
+ 2. Add a `${key}Value` property that enables TypeScript narrowing
278
+ 3. Provide a type-safe `set()` method for programmatic updates
279
+ 4. Fix `.as("radio", value)` to accept only valid discriminator values
280
+
281
+ The following example demonstrates conditionally rendering variant-specific fields with type-safe narrowing, without using `FieldVariants`. This approach requires JavaScript (unlike `FieldVariants` which works without JS):
282
+
283
+ ```svelte
284
+ <script lang="ts">
285
+ import { shapeForm } from './data.remote';
286
+ import { discriminated } from 'sveltekit-discriminated-fields';
287
+
288
+ const shape = $derived(discriminated('kind', shapeForm.fields));
289
+ </script>
290
+
291
+ <!-- Use kindValue (not kind.value()) for type narrowing -->
292
+ {#if shape.kindValue === 'circle'}
293
+ <input {...shape.radius.as('number')} /> <!-- TypeScript knows radius exists -->
294
+ {:else if shape.kindValue === 'rectangle'}
295
+ <input {...shape.width.as('number')} /> <!-- TypeScript knows width exists -->
296
+ <input {...shape.height.as('number')} />
138
297
  {/if}
139
298
  ```
140
299
 
300
+ See the [programmatic-form example](./test/src/routes/programmatic-form) for usage of `set()` and other programmatic features.
301
+
141
302
  ## API
142
303
 
143
- ### `discriminatedFields(key, fields)`
304
+ ### `FieldVariants`
305
+
306
+ A component for rendering variant-specific form sections with CSS-only visibility.
307
+
308
+ **Props:**
309
+
310
+ | Prop | Type | Description |
311
+ | --------- | --------------------- | --------------------------------------------------------- |
312
+ | `fields` | `DiscriminatedFields` | Form fields (raw or wrapped with `discriminated()`) |
313
+ | `key` | `string` | The discriminator key (must match a field in the schema) |
314
+ | `partial` | `boolean` (optional) | Allow missing snippets for some variants (default: false) |
315
+ | `css` | `boolean` (optional) | Enable CSS visibility generation (default: true) |
316
+
317
+ **Snippets:**
318
+
319
+ - `fallback(props)` - Rendered when no variant is selected. Spread `props` onto your element.
320
+ - `{variantName}(v)` - One snippet per variant. Spread `v` onto your container, access fields via `v.fields`.
321
+
322
+ ### `discriminated(key, fields)`
144
323
 
145
324
  Wraps discriminated union form fields for type-safe narrowing.
146
325
 
147
326
  **Parameters:**
148
327
 
149
- - `key` - The discriminator key (e.g., `'type'`, `'kind'`, `'channel'`)
328
+ - `key` - The discriminator key (must exist as a field in all variants)
150
329
  - `fields` - Form fields from a discriminated union schema
151
330
 
152
331
  **Returns:** A proxy object with:
153
332
 
154
333
  - All original fields passed through unchanged
155
334
  - `${key}Value` - The current discriminator value (for narrowing)
156
- - `set()` - Type-safe setter that infers variant from discriminator
335
+ - `set(data)` - Type-safe setter that infers variant from discriminator
336
+ - `allIssues()` - All validation issues for the discriminated fields
157
337
 
158
338
  ### `DiscriminatedData<T>`
159
339
 
160
340
  Type helper that extracts the underlying data type from wrapped fields:
161
341
 
162
342
  ```typescript
163
- const payment = discriminatedFields('type', form.fields);
343
+ const payment = discriminated("type", form.fields);
164
344
  type Payment = DiscriminatedData<typeof payment>;
165
- // { type: 'card'; cardNumber: string; cvv: string } | { type: 'bank'; accountNumber: string; sortCode: string }
345
+ // { type: 'card'; cardNumber: string; cvv: string } | { type: 'bank'; ... }
346
+ ```
347
+
348
+ ### `VariantSnippetArg<T>`
349
+
350
+ Type for the argument passed to variant snippets:
351
+
352
+ ```typescript
353
+ // v can be spread onto elements and has a .fields property
354
+ type VariantSnippetArg<T> = VariantProps & { readonly fields: T };
166
355
  ```
167
356
 
168
- Useful when you need to reference the data type elsewhere, such as defining default values or passing data to other functions.
357
+ ## Migration from v0.1
169
358
 
170
- ## Note on Runtime Behavior
359
+ If upgrading from v0.1:
171
360
 
172
- Type narrowing is compile-time only. At runtime, `'width' in shape` will return `true` even when `kindValue === 'circle'`, because all variant fields exist on the underlying object.
361
+ 1. Rename `UnionVariants` to `FieldVariants`
362
+ 2. Rename `discriminatedFields()` to `discriminated()`
363
+ 3. Remove the `selector` prop (no longer needed - `form:has()` handles all layouts)
364
+ 4. Update snippet signatures:
365
+ - Old: `{#snippet circle(fields)} <input {...fields.radius.as('number')} />`
366
+ - New: `{#snippet circle(v)} <div {...v}><input {...v.fields.radius.as('number')} /></div>`
367
+ 5. Add `props` parameter to fallback and spread it: `{#snippet fallback(props)} <p {...props}>...</p>`
173
368
 
174
369
  ## License
175
370
 
@@ -0,0 +1,234 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { RemoteFormField, RemoteFormFieldValue, RemoteFormFields } from "@sveltejs/kit";
4
+
5
+ // =============================================================================
6
+ // Core types - work with data type D instead of field object type T
7
+ // =============================================================================
8
+
9
+ // Extract data type from fields' set method
10
+ type Data<T> = T extends { set: (v: infer D) => unknown } ? D : never;
11
+
12
+ // Discriminator values from data (distributes over D)
13
+ type Values<K extends string, D> = D extends Record<K, infer V> ? (V extends string ? V : never) : never;
14
+
15
+ // Narrow D to variant where discriminator equals V
16
+ type NarrowData<K extends string, D, V> = D extends Record<K, V> ? D : never;
17
+
18
+ // Common keys (in EVERY variant) - for a union, keyof gives intersection
19
+ type CommonKeys<D> = keyof D;
20
+
21
+ // =============================================================================
22
+ // Validation - detect problematic field configurations
23
+ // =============================================================================
24
+
25
+ // Get all keys from ALL variants (distributes over union to collect all keys)
26
+ type AllDataKeys<D> = D extends D ? keyof D : never;
27
+
28
+ // Variants containing key P (distributes to filter union members)
29
+ // Checks if P is in keyof D (works for both required and optional properties)
30
+ type VariantsWithKey<D, P> = D extends D ? (P extends keyof D ? D : never) : never;
31
+
32
+ // Check if T is a union type (multiple members vs single type)
33
+ type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
34
+
35
+ // Key P is "partially shared" if:
36
+ // - NOT in all variants (not in CommonKeys)
37
+ // - In MORE THAN ONE variant (VariantsWithKey is a union)
38
+ // Keys in exactly 1 variant are fine (variant-specific)
39
+ type IsPartiallyShared<D, P> = P extends CommonKeys<D>
40
+ ? false // In all variants - OK (common field)
41
+ : IsUnion<VariantsWithKey<D, P>>; // true if 2+ variants have this key
42
+
43
+ // Find all partially shared keys (in 2 to N-1 variants, not variant-specific)
44
+ type PartiallySharedKeys<K extends string, D> = {
45
+ [P in Exclude<AllDataKeys<D>, K>]: IsPartiallyShared<D, P> extends true ? P : never;
46
+ }[Exclude<AllDataKeys<D>, K>];
47
+
48
+ // Check if types match across variants for a given key
49
+ type TypeAt<D, P extends PropertyKey> = D extends Record<P, infer V> ? V : never;
50
+
51
+ // For mixed type detection, check if the type varies across variants
52
+ type HasMixedTypes<D, P extends PropertyKey> = IsUnion<TypeAt<D, P>>;
53
+
54
+ // Common keys with different types across variants
55
+ type MixedTypeKeys<K extends string, D, CK extends PropertyKey = Exclude<CommonKeys<D>, K>> = CK extends CK
56
+ ? HasMixedTypes<D, CK> extends true
57
+ ? CK
58
+ : never
59
+ : never;
60
+
61
+ // Validation - check for both partially shared keys AND mixed types
62
+ type Validate<K extends string, D, T> = [PartiallySharedKeys<K, D>] extends [never]
63
+ ? [MixedTypeKeys<K, D>] extends [never]
64
+ ? T
65
+ : `Error: Field '${MixedTypeKeys<K, D> & string}' has different types across variants. Common fields must have the same type in all variants.`
66
+ : `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.`;
67
+
68
+ // =============================================================================
69
+ // Snippet types - build field objects from data
70
+ // =============================================================================
71
+
72
+ // Reserved prop names
73
+ type Reserved = "fields" | "key" | "fallback" | "partial" | "css";
74
+
75
+ // Build field type - uses SvelteKit's RemoteFormFields for nested objects
76
+ // This correctly includes set(), value(), issues(), allIssues() on nested containers
77
+ type Field<V> = [V] extends [object]
78
+ ? RemoteFormFields<V>
79
+ : RemoteFormField<V & RemoteFormFieldValue>;
80
+
81
+ // Variant snippet fields: narrowed data → field object, excluding common fields
82
+ type SnippetFields<K extends string, D, V extends string, Narrow = NarrowData<K, D, V>> = {
83
+ readonly [P in Exclude<keyof Narrow, K | CommonKeys<D>>]: Field<Narrow[P & keyof Narrow]>;
84
+ } & {
85
+ readonly [P in K]: Field<V>;
86
+ };
87
+
88
+ // Props passed to variant snippets for CSS targeting (spread onto wrapper element)
89
+ export type VariantProps = { "data-fv"?: string };
90
+
91
+ // Combined variant snippet argument - spread for props, access .fields for variant data
92
+ // Mirrors form pattern: <form {...form}> + form.fields.x → <div {...v}> + v.fields.x
93
+ export type VariantSnippetArg<F> = VariantProps & { readonly fields: F };
94
+
95
+ // Snippet props - one snippet per variant, receives single arg with .fields property
96
+ type Snippets<K extends string, D, V extends string, IsPartial extends boolean> = IsPartial extends true
97
+ ? { [P in Exclude<V, Reserved>]?: Snippet<[VariantSnippetArg<SnippetFields<K, D, P>>]> }
98
+ : { [P in Exclude<V, Reserved>]: Snippet<[VariantSnippetArg<SnippetFields<K, D, P>>]> };
99
+
100
+ // =============================================================================
101
+ // Component props
102
+ // =============================================================================
103
+
104
+ export type FieldVariantsProps<
105
+ K extends string,
106
+ T extends { set: (v: never) => unknown } & Record<K, { value(): unknown }>,
107
+ V extends string = Values<K, Data<T>>,
108
+ IsPartial extends boolean = false,
109
+ > = {
110
+ fields: Validate<K, Data<T>, T>;
111
+ key: K;
112
+ /** Optional snippet shown when no variant is selected. Receives (props) for CSS targeting. */
113
+ fallback?: Snippet<[VariantProps]>;
114
+ /** When true, variant snippets are optional (default: false) */
115
+ partial?: IsPartial;
116
+ /** Set to false to disable CSS generation (default: true) */
117
+ css?: boolean;
118
+ } & Snippets<K, Data<T>, V, IsPartial>;
119
+ </script>
120
+
121
+ <script
122
+ lang="ts"
123
+ generics="K extends string, T extends { set: (v: never) => unknown } & Record<K, { value(): unknown }>, V extends string = Values<K, Data<T>>, IsPartial extends boolean = false"
124
+ >
125
+ import { onMount } from "svelte";
126
+
127
+ type Props = FieldVariantsProps<K, T, V, IsPartial>;
128
+
129
+ let { fields, key, fallback, partial, css: cssEnabled = true, ...snippets }: Props = $props();
130
+
131
+ // Get all variant values from the snippet names
132
+ const variantValues = Object.keys(snippets) as V[];
133
+
134
+ // After hydration, switch from CSS-only to JS-based conditional rendering
135
+ // This enables Svelte transitions and other JS-dependent features
136
+ let hydrated = $state(false);
137
+ onMount(() => {
138
+ hydrated = true;
139
+ });
140
+
141
+ // Get the current discriminator value
142
+ const currentValue = $derived.by(() => {
143
+ const discriminatorField = (fields as Record<K, { value(): unknown }>)[key];
144
+ return discriminatorField?.value() as V | "" | undefined;
145
+ });
146
+
147
+ // Get the actual field name from the discriminator field (used for CSS selectors)
148
+ const fieldName = $derived.by(() => {
149
+ const discriminatorField = (fields as Record<K, { as(type: "select"): { name: string } }>)[key];
150
+ return discriminatorField?.as("select")?.name ?? key;
151
+ });
152
+
153
+ // Generate CSS for showing/hiding variants based on select or radio value
154
+ // Uses form:has() to scope to the containing form - works regardless of DOM structure
155
+ const css = $derived.by(() => {
156
+ if (!cssEnabled) return "";
157
+
158
+ const name = fieldName;
159
+ // Use .fieldName.variant format - the leading dot prevents collisions
160
+ const fv = (v: string) => `[data-fv=".${name}.${v}"]`;
161
+ const fvPrefix = `[data-fv^=".${name}."]`;
162
+
163
+ // form:has() scopes to the containing form, name-based targeting is precise
164
+ const sel = (v: string) => `form:has(select[name="${name}"] option[value="${v}"]:checked)`;
165
+ const rad = (v: string) => `form:has(input[name="${name}"][value="${v}"]:checked)`;
166
+
167
+ // Hide all variant sections by default (starts-with selector matches all variants for this field)
168
+ const hideAll = `form:has([name="${name}"]) ${fvPrefix}:not(${fv("fallback")}) { display: none; }\n`;
169
+
170
+ // Show the variant section that matches the selected value
171
+ const showVariants = variantValues
172
+ .map((v) => `${sel(v)} ${fv(v)},
173
+ ${rad(v)} ${fv(v)} { display: contents; }`)
174
+ .join("\n");
175
+
176
+ // Hide fallback when ANY value is selected (not just known variants)
177
+ // This checks for any non-empty option selected or any radio checked
178
+ const hideFallback = fallback
179
+ ? `form:has(select[name="${name}"] option:checked:not([value=""])) ${fv("fallback")},
180
+ form:has(input[name="${name}"]:checked) ${fv("fallback")} { display: none; }\n`
181
+ : "";
182
+
183
+ return hideAll + showVariants + hideFallback;
184
+ });
185
+
186
+ // Props to add to wrapper elements when CSS is enabled - uses .fieldName.variant format
187
+ const wrapperProps = (variant: string): VariantProps =>
188
+ cssEnabled ? { "data-fv": `.${fieldName}.${variant}` } : {};
189
+
190
+ // Create snippet argument with non-enumerable fields property
191
+ // This allows {...v} to only spread the CSS props, while v.fields is still accessible
192
+ const createSnippetArg = (variant: string, variantFields: T): VariantSnippetArg<T> => {
193
+ const arg = { ...wrapperProps(variant) };
194
+ Object.defineProperty(arg, "fields", {
195
+ value: variantFields,
196
+ enumerable: false,
197
+ configurable: false,
198
+ writable: false,
199
+ });
200
+ return arg as VariantSnippetArg<T>;
201
+ };
202
+ </script>
203
+
204
+ <svelte:head>
205
+ {#if css && !hydrated}
206
+ <!-- CSS-only visibility before JS hydrates -->
207
+ {@html `<style>${css}</style>`}
208
+ {/if}
209
+ </svelte:head>
210
+
211
+ {#if hydrated}
212
+ <!-- After hydration: JS-based conditional rendering (enables transitions) -->
213
+ <!-- Fallback shows when no value is selected (empty string or undefined) -->
214
+ {#if !currentValue && fallback}
215
+ {@render fallback(wrapperProps("fallback"))}
216
+ {/if}
217
+
218
+ {#each variantValues as v (v)}
219
+ {#if currentValue === v}
220
+ {@const snippet = (snippets as unknown as Record<V, Snippet<[VariantSnippetArg<T>]>>)[v]}
221
+ {@render snippet(createSnippetArg(v, fields as T))}
222
+ {/if}
223
+ {/each}
224
+ {:else}
225
+ <!-- Before hydration: render all, CSS handles visibility -->
226
+ {#if fallback}
227
+ {@render fallback(wrapperProps("fallback"))}
228
+ {/if}
229
+
230
+ {#each variantValues as v (v)}
231
+ {@const snippet = (snippets as unknown as Record<V, Snippet<[VariantSnippetArg<T>]>>)[v]}
232
+ {@render snippet(createSnippetArg(v, fields as T))}
233
+ {/each}
234
+ {/if}
@@ -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,39 +1,3 @@
1
- export type DiscriminatedData<T> = T extends {
2
- set: (v: infer D) => unknown;
3
- } ? D : never;
4
- type Setter<K extends string, T, D = DiscriminatedData<T>> = {
5
- set: <V extends D[K & keyof D]>(data: Extract<D, {
6
- [P in K]: V;
7
- }>) => void;
8
- };
9
- type Keys<T> = T extends unknown ? keyof T : never;
10
- type KeyValue<K extends string, V> = {
11
- [P in `${K}Value`]: V;
12
- };
13
- type Fields<T> = {
14
- [P in Keys<T>]: T extends Record<P, infer V> ? V : never;
15
- };
16
- type Variant<K extends string, T, V> = KeyValue<K, V> & Fields<T>;
17
- type Narrowed<K extends string, T> = T extends {
18
- [P in K]: {
19
- value(): infer V;
20
- };
21
- } ? Variant<K, T, V> : never;
22
- type Union<K extends string, T> = Narrowed<K, T> | Variant<K, T, undefined>;
23
- type DiscriminatedFields<K extends string, T> = Setter<K, T> & Union<K, T>;
24
- /**
25
- * Wraps discriminated union form fields, adding ${key}Value for type narrowing.
26
- * - All original fields pass through unchanged (type, issues, allIssues, etc.)
27
- * - `set` is overridden with type-safe version
28
- * - `${key}Value` is added for discriminator value (e.g., `reward.typeValue`)
29
- *
30
- * @param key - Discriminator key (e.g. 'type')
31
- * @param fields - Form fields from a discriminated union schema
32
- * @returns Passthrough object with type-safe set() and ${key}Value for narrowing
33
- */
34
- export declare function discriminatedFields<K extends string, T extends {
35
- set: (v: never) => unknown;
36
- } & Record<K, {
37
- value(): unknown;
38
- }>>(key: K, fields: T): DiscriminatedFields<K, T>;
39
- export {};
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,25 +1,4 @@
1
- /**
2
- * Wraps discriminated union form fields, adding ${key}Value for type narrowing.
3
- * - All original fields pass through unchanged (type, issues, allIssues, etc.)
4
- * - `set` is overridden with type-safe version
5
- * - `${key}Value` is added for discriminator value (e.g., `reward.typeValue`)
6
- *
7
- * @param key - Discriminator key (e.g. 'type')
8
- * @param fields - Form fields from a discriminated union schema
9
- * @returns Passthrough object with type-safe set() and ${key}Value for narrowing
10
- */
11
- export function discriminatedFields(key, fields) {
12
- const proxy = new Proxy(fields, {
13
- get(target, prop) {
14
- if (prop === `${key}Value`)
15
- return target[key].value();
16
- if (prop === 'set')
17
- return (data) => target.set(data);
18
- return Reflect.get(target, prop);
19
- },
20
- has(target, prop) {
21
- return prop === `${key}Value` || prop in target;
22
- }
23
- });
24
- return proxy;
25
- }
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,11 +1,14 @@
1
1
  {
2
2
  "name": "sveltekit-discriminated-fields",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Type-safe discriminated union support for SvelteKit form fields",
5
5
  "type": "module",
6
+ "svelte": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
6
8
  "exports": {
7
9
  ".": {
8
10
  "types": "./dist/index.d.ts",
11
+ "svelte": "./dist/index.js",
9
12
  "default": "./dist/index.js"
10
13
  }
11
14
  },
@@ -13,11 +16,10 @@
13
16
  "dist"
14
17
  ],
15
18
  "scripts": {
16
- "build": "tsc -p tsconfig.build.json",
17
- "check": "tsc --noEmit",
18
- "test:types": "tsc -p test/types/tsconfig.json",
19
- "test:runtime": "npm run build && tsc -p test/runtime/tsconfig.json && npx tsx test/runtime/has.test.ts",
20
- "test": "npm run test:types && npm run test:runtime",
19
+ "build": "svelte-package -i src -o dist",
20
+ "check": "svelte-check --tsconfig ./tsconfig.json",
21
+ "test:types": "npm run --prefix test build && svelte-check --tsconfig ./test/tsconfig.json",
22
+ "test": "npm run test:types && playwright test",
21
23
  "prepublishOnly": "npm run test && npm run build"
22
24
  },
23
25
  "keywords": [
@@ -33,7 +35,18 @@
33
35
  "type": "git",
34
36
  "url": "https://github.com/robertadamsonsmith/sveltekit-discriminated-fields"
35
37
  },
38
+ "peerDependencies": {
39
+ "@sveltejs/kit": "^2.27.0",
40
+ "svelte": "^5.0.0"
41
+ },
36
42
  "devDependencies": {
43
+ "@playwright/test": "^1.57.0",
44
+ "@sveltejs/package": "^2.0.0",
45
+ "@types/node": "^25.0.3",
46
+ "@typescript-eslint/types": "^8.51.0",
47
+ "playwright": "^1.57.0",
48
+ "svelte": "^5.0.0",
49
+ "svelte-check": "^4.0.0",
37
50
  "typescript": "^5.0.0"
38
51
  }
39
52
  }