@samuel-charpentier/sform 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/dist/Sform/Sfield.svelte +164 -0
  4. package/dist/Sform/Sfield.svelte.d.ts +4 -0
  5. package/dist/Sform/Sform.svelte +101 -0
  6. package/dist/Sform/Sform.svelte.d.ts +51 -0
  7. package/dist/Sform/context.svelte.d.ts +3 -0
  8. package/dist/Sform/context.svelte.js +65 -0
  9. package/dist/Sform/index.d.ts +5 -0
  10. package/dist/Sform/index.js +7 -0
  11. package/dist/Sform/inputs/ButtonInput.svelte +75 -0
  12. package/dist/Sform/inputs/ButtonInput.svelte.d.ts +32 -0
  13. package/dist/Sform/inputs/CheckboxGroupInput.svelte +81 -0
  14. package/dist/Sform/inputs/CheckboxGroupInput.svelte.d.ts +4 -0
  15. package/dist/Sform/inputs/CheckboxInput.svelte +47 -0
  16. package/dist/Sform/inputs/CheckboxInput.svelte.d.ts +4 -0
  17. package/dist/Sform/inputs/MaskedInput.svelte +220 -0
  18. package/dist/Sform/inputs/MaskedInput.svelte.d.ts +4 -0
  19. package/dist/Sform/inputs/NumberInput.svelte +116 -0
  20. package/dist/Sform/inputs/NumberInput.svelte.d.ts +4 -0
  21. package/dist/Sform/inputs/PasswordInput.svelte +142 -0
  22. package/dist/Sform/inputs/PasswordInput.svelte.d.ts +4 -0
  23. package/dist/Sform/inputs/RadioInput.svelte +81 -0
  24. package/dist/Sform/inputs/RadioInput.svelte.d.ts +4 -0
  25. package/dist/Sform/inputs/RangeInput.svelte +67 -0
  26. package/dist/Sform/inputs/RangeInput.svelte.d.ts +4 -0
  27. package/dist/Sform/inputs/SelectInput.svelte +36 -0
  28. package/dist/Sform/inputs/SelectInput.svelte.d.ts +4 -0
  29. package/dist/Sform/inputs/TextInput.svelte +61 -0
  30. package/dist/Sform/inputs/TextInput.svelte.d.ts +4 -0
  31. package/dist/Sform/inputs/TextareaInput.svelte +56 -0
  32. package/dist/Sform/inputs/TextareaInput.svelte.d.ts +4 -0
  33. package/dist/Sform/inputs/ToggleInput.svelte +101 -0
  34. package/dist/Sform/inputs/ToggleInput.svelte.d.ts +4 -0
  35. package/dist/Sform/inputs/ToggleOptionsInput.svelte +118 -0
  36. package/dist/Sform/inputs/ToggleOptionsInput.svelte.d.ts +4 -0
  37. package/dist/Sform/sform.css +628 -0
  38. package/dist/Sform/types.d.ts +610 -0
  39. package/dist/Sform/types.js +5 -0
  40. package/dist/Sform/utils/mask.d.ts +68 -0
  41. package/dist/Sform/utils/mask.js +155 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +1 -0
  44. package/package.json +78 -0
@@ -0,0 +1,51 @@
1
+ import type { RemoteFormInstance, ValidateOn, EnhanceCallback } from './types.js';
2
+ import type { RemoteFormFields } from '@sveltejs/kit';
3
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
4
+ import type { Snippet } from 'svelte';
5
+ declare function $$render<Input extends import('@sveltejs/kit').RemoteFormInput, Output>(): {
6
+ props: {
7
+ /** Remote form object from form() API, or the result of form.for(id) */
8
+ form: RemoteFormInstance<Input, Output>;
9
+ /** Preflight validation schema (Valibot, Zod, or any StandardSchema) */
10
+ schema?: StandardSchemaV1<Input, unknown>;
11
+ /** Enhance callback for custom form submission handling */
12
+ enhance?: EnhanceCallback<Input>;
13
+ /** When to validate and show issues: 'blur' (default), 'change', or 'submit' */
14
+ validateOn?: ValidateOn;
15
+ /** Form element class */
16
+ class?: string;
17
+ /**
18
+ * Children snippet receives typed fields for type-safe field access.
19
+ * @example
20
+ * ```svelte
21
+ * <Sform {form}>
22
+ * {#snippet children(fields)}
23
+ * <Sfield field={fields.username} type="text" />
24
+ * {/snippet}
25
+ * </Sform>
26
+ * ```
27
+ */
28
+ children: Snippet<[RemoteFormFields<Input>]>;
29
+ };
30
+ exports: {};
31
+ bindings: "";
32
+ slots: {};
33
+ events: {};
34
+ };
35
+ declare class __sveltets_Render<Input extends import('@sveltejs/kit').RemoteFormInput, Output> {
36
+ props(): ReturnType<typeof $$render<Input, Output>>['props'];
37
+ events(): ReturnType<typeof $$render<Input, Output>>['events'];
38
+ slots(): ReturnType<typeof $$render<Input, Output>>['slots'];
39
+ bindings(): "";
40
+ exports(): {};
41
+ }
42
+ interface $$IsomorphicComponent {
43
+ new <Input extends import('@sveltejs/kit').RemoteFormInput, Output>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Input, Output>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Input, Output>['props']>, ReturnType<__sveltets_Render<Input, Output>['events']>, ReturnType<__sveltets_Render<Input, Output>['slots']>> & {
44
+ $$bindings?: ReturnType<__sveltets_Render<Input, Output>['bindings']>;
45
+ } & ReturnType<__sveltets_Render<Input, Output>['exports']>;
46
+ <Input extends import('@sveltejs/kit').RemoteFormInput, Output>(internal: unknown, props: ReturnType<__sveltets_Render<Input, Output>['props']> & {}): ReturnType<__sveltets_Render<Input, Output>['exports']>;
47
+ z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
48
+ }
49
+ declare const Sform: $$IsomorphicComponent;
50
+ type Sform<Input extends import('@sveltejs/kit').RemoteFormInput, Output> = InstanceType<typeof Sform<Input, Output>>;
51
+ export default Sform;
@@ -0,0 +1,3 @@
1
+ import type { SformContext, ValidateOn } from './types.js';
2
+ export declare function createSformContext(getValidateOn: () => ValidateOn, getFieldNames: () => string[], triggerValidation: () => void): SformContext;
3
+ export declare function getSformContext(): SformContext;
@@ -0,0 +1,65 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { SvelteSet } from 'svelte/reactivity';
3
+ const SFORM_CONTEXT_KEY = Symbol('sform-context');
4
+ export function createSformContext(getValidateOn, getFieldNames, triggerValidation) {
5
+ const touched = new SvelteSet();
6
+ const dirty = new SvelteSet();
7
+ let submitted = $state(false);
8
+ const context = {
9
+ get validateOn() {
10
+ return getValidateOn();
11
+ },
12
+ get submitted() {
13
+ return submitted;
14
+ },
15
+ triggerValidation,
16
+ getFieldState: (name) => ({
17
+ touched: touched.has(name),
18
+ dirty: dirty.has(name)
19
+ }),
20
+ markTouched: (name) => {
21
+ touched.add(name);
22
+ },
23
+ markDirty: (name) => {
24
+ dirty.add(name);
25
+ },
26
+ shouldDisplayIssues: (name, fieldValidateOn) => {
27
+ const effectiveValidateOn = fieldValidateOn ?? getValidateOn();
28
+ switch (effectiveValidateOn) {
29
+ case 'blur':
30
+ return touched.has(name);
31
+ case 'change':
32
+ return dirty.has(name);
33
+ case 'submit':
34
+ return submitted;
35
+ default:
36
+ return false;
37
+ }
38
+ },
39
+ markSubmitted: () => {
40
+ submitted = true;
41
+ },
42
+ markAllFieldsDirty: () => {
43
+ // Get all field names from the form and mark them as touched and dirty
44
+ const fieldNames = getFieldNames();
45
+ for (const name of fieldNames) {
46
+ touched.add(name);
47
+ dirty.add(name);
48
+ }
49
+ },
50
+ resetFieldStates: () => {
51
+ touched.clear();
52
+ dirty.clear();
53
+ submitted = false;
54
+ }
55
+ };
56
+ setContext(SFORM_CONTEXT_KEY, context);
57
+ return context;
58
+ }
59
+ export function getSformContext() {
60
+ const context = getContext(SFORM_CONTEXT_KEY);
61
+ if (!context) {
62
+ throw new Error('Sfield must be used within an Sform component');
63
+ }
64
+ return context;
65
+ }
@@ -0,0 +1,5 @@
1
+ export { default as Sform } from './Sform.svelte';
2
+ export { default as Sfield } from './Sfield.svelte';
3
+ export { default as Sbutton } from './inputs/ButtonInput.svelte';
4
+ export { applyMask, unmask, MASK_PATTERNS, DEFAULT_TOKENS, type MaskOptions, type MaskResult, type MaskToken, type MaskPattern } from './utils/mask.js';
5
+ export type { ValidateOn, FieldState, SfieldClasses, InputType, SelectOption, SformContext, SformProps, ButtonFormState, ButtonInputProps, InputAffixProps, RangeInputProps, ToggleInputProps, ToggleOption, ToggleOptionsInputProps, SfieldTypeMap, AllowedSfieldType, TypedSfieldProps, SfieldBaseProps, TypedBaseSfieldProps, SfieldTextProps, SfieldPasswordProps, SfieldNumberProps, SfieldTextareaProps, SfieldSelectProps, SfieldCheckboxProps, SfieldCheckboxGroupProps, SfieldRadioProps, SfieldRangeProps, SfieldToggleProps, SfieldToggleOptionsProps, SfieldMaskedProps, RemoteForm, RemoteFormField, RemoteFormFields, RemoteFormFieldValue, RemoteFormInput, RemoteFormIssue } from './types.js';
@@ -0,0 +1,7 @@
1
+ // Main components
2
+ export { default as Sform } from './Sform.svelte';
3
+ export { default as Sfield } from './Sfield.svelte';
4
+ // Standalone components (not routed through Sfield)
5
+ export { default as Sbutton } from './inputs/ButtonInput.svelte';
6
+ // Utilities
7
+ export { applyMask, unmask, MASK_PATTERNS, DEFAULT_TOKENS } from './utils/mask.js';
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { ButtonFormState, RemoteFormIssue } from '../types.js';
4
+
5
+ interface FormLike {
6
+ pending?: number;
7
+ result?: unknown;
8
+ fields: {
9
+ allIssues?: () => RemoteFormIssue[] | undefined;
10
+ };
11
+ }
12
+
13
+ interface Props {
14
+ /** The remote form - required for button state */
15
+ form: FormLike;
16
+ /** Button text (used if no snippets provided) */
17
+ label?: string;
18
+ /** Button type */
19
+ buttonType?: 'submit' | 'reset' | 'button';
20
+ /** Button class */
21
+ class?: string;
22
+ /** Whether button is disabled */
23
+ disabled?: boolean;
24
+ /** Snippet for default state */
25
+ defaultState?: Snippet<[ButtonFormState]>;
26
+ /** Snippet for pending state */
27
+ pendingState?: Snippet<[ButtonFormState]>;
28
+ /** Snippet for success state */
29
+ successState?: Snippet<[ButtonFormState]>;
30
+ /** Snippet for error/issues state */
31
+ errorState?: Snippet<[ButtonFormState]>;
32
+ }
33
+
34
+ let {
35
+ form,
36
+ label = 'Submit',
37
+ buttonType = 'submit',
38
+ class: className,
39
+ disabled = false,
40
+ defaultState,
41
+ pendingState,
42
+ successState,
43
+ errorState
44
+ }: Props = $props();
45
+
46
+ const formState: ButtonFormState = $derived.by(() => {
47
+ const pending = (form.pending ?? 0) !== 0;
48
+ const hasResult = form.result !== undefined;
49
+ const issues = form.fields.allIssues?.() ?? [];
50
+ const hasIssues = issues.length > 0;
51
+
52
+ return {
53
+ pending,
54
+ success: hasResult && !hasIssues && !pending,
55
+ hasIssues,
56
+ result: form.result
57
+ };
58
+ });
59
+
60
+ const isDisabled = $derived(disabled || formState.pending);
61
+ </script>
62
+
63
+ <button type={buttonType} class={className} disabled={isDisabled}>
64
+ {#if formState.pending && pendingState}
65
+ {@render pendingState(formState)}
66
+ {:else if formState.success && successState}
67
+ {@render successState(formState)}
68
+ {:else if formState.hasIssues && errorState}
69
+ {@render errorState(formState)}
70
+ {:else if defaultState}
71
+ {@render defaultState(formState)}
72
+ {:else}
73
+ {label}
74
+ {/if}
75
+ </button>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ButtonFormState, RemoteFormIssue } from '../types.js';
3
+ interface FormLike {
4
+ pending?: number;
5
+ result?: unknown;
6
+ fields: {
7
+ allIssues?: () => RemoteFormIssue[] | undefined;
8
+ };
9
+ }
10
+ interface Props {
11
+ /** The remote form - required for button state */
12
+ form: FormLike;
13
+ /** Button text (used if no snippets provided) */
14
+ label?: string;
15
+ /** Button type */
16
+ buttonType?: 'submit' | 'reset' | 'button';
17
+ /** Button class */
18
+ class?: string;
19
+ /** Whether button is disabled */
20
+ disabled?: boolean;
21
+ /** Snippet for default state */
22
+ defaultState?: Snippet<[ButtonFormState]>;
23
+ /** Snippet for pending state */
24
+ pendingState?: Snippet<[ButtonFormState]>;
25
+ /** Snippet for success state */
26
+ successState?: Snippet<[ButtonFormState]>;
27
+ /** Snippet for error/issues state */
28
+ errorState?: Snippet<[ButtonFormState]>;
29
+ }
30
+ declare const ButtonInput: import("svelte").Component<Props, {}, "">;
31
+ type ButtonInput = ReturnType<typeof ButtonInput>;
32
+ export default ButtonInput;
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import type { CheckboxGroupInputProps, SelectOption } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ class: className,
9
+ labelClass,
10
+ disabled,
11
+ options = [],
12
+ showIssues,
13
+ onblur,
14
+ oninput
15
+ }: CheckboxGroupInputProps = $props();
16
+
17
+ // Normalize options to SelectOption format
18
+ const normalizedOptions = $derived(
19
+ options.map((opt): SelectOption => (typeof opt === 'string' ? { value: opt, label: opt } : opt))
20
+ );
21
+
22
+ // Helper to get field attrs with controlled aria-invalid
23
+ function getFieldAttrs(optionValue: string) {
24
+ const attrs = field.as('checkbox', optionValue);
25
+ return {
26
+ ...attrs,
27
+ 'aria-invalid': showIssues ? attrs['aria-invalid'] : undefined
28
+ };
29
+ }
30
+ </script>
31
+
32
+ <fieldset class="sform-checkbox-group {className ?? ''}" {disabled}>
33
+ {#if label}
34
+ <legend class={labelClass}>{label}</legend>
35
+ {/if}
36
+ {#each normalizedOptions as option}
37
+ {@const fieldAttrs = getFieldAttrs(option.value)}
38
+ {@const uniqueId = `${name}-${option.value}`}
39
+ <label class="sform-checkbox-option" class:disabled={option.disabled}>
40
+ <input
41
+ {...fieldAttrs}
42
+ type="checkbox"
43
+ id={uniqueId}
44
+ disabled={disabled || option.disabled}
45
+ {onblur}
46
+ {oninput}
47
+ />
48
+ <span class="sform-checkbox-label">{option.label}</span>
49
+ </label>
50
+ {/each}
51
+ </fieldset>
52
+
53
+ <style>
54
+ .sform-checkbox-group {
55
+ border: none;
56
+ padding: 0;
57
+ margin: 0;
58
+ }
59
+
60
+ .sform-checkbox-group legend {
61
+ padding: 0;
62
+ margin-bottom: 0.5rem;
63
+ }
64
+
65
+ .sform-checkbox-option {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.5rem;
69
+ cursor: pointer;
70
+ padding: 0.25rem 0;
71
+ }
72
+
73
+ .sform-checkbox-option.disabled {
74
+ opacity: 0.6;
75
+ cursor: not-allowed;
76
+ }
77
+
78
+ .sform-checkbox-option input {
79
+ margin: 0;
80
+ }
81
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { CheckboxGroupInputProps } from '../types.js';
2
+ declare const CheckboxGroupInput: import("svelte").Component<CheckboxGroupInputProps, {}, "">;
3
+ type CheckboxGroupInput = ReturnType<typeof CheckboxGroupInput>;
4
+ export default CheckboxGroupInput;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { CheckboxRadioInputProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ class: className,
9
+ labelClass,
10
+ disabled,
11
+ value,
12
+ showIssues,
13
+ onblur,
14
+ oninput
15
+ }: CheckboxRadioInputProps = $props();
16
+
17
+ // Single boolean checkbox - no value needed
18
+ const fieldAttrs = $derived({
19
+ ...field.as('checkbox'),
20
+ 'aria-invalid': showIssues ? field.as('checkbox')['aria-invalid'] : undefined
21
+ });
22
+ </script>
23
+
24
+ <label class="sform-checkbox {className ?? ''}" class:disabled>
25
+ <input {...fieldAttrs} type="checkbox" id={name} {disabled} {onblur} {oninput} />
26
+ {#if label}
27
+ <span class={labelClass}>{label}</span>
28
+ {/if}
29
+ </label>
30
+
31
+ <style>
32
+ .sform-checkbox {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ gap: 0.5rem;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .sform-checkbox.disabled {
40
+ opacity: 0.6;
41
+ cursor: not-allowed;
42
+ }
43
+
44
+ .sform-checkbox input {
45
+ margin: 0;
46
+ }
47
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { CheckboxRadioInputProps } from '../types.js';
2
+ declare const CheckboxInput: import("svelte").Component<CheckboxRadioInputProps, {}, "">;
3
+ type CheckboxInput = ReturnType<typeof CheckboxInput>;
4
+ export default CheckboxInput;
@@ -0,0 +1,220 @@
1
+ <script lang="ts">
2
+ import type { MaskedInputProps } from '../types.js';
3
+ import { applyMask, MASK_PATTERNS, type MaskPattern, type MaskToken } from '../utils/mask.js';
4
+
5
+ let {
6
+ field,
7
+ name,
8
+ label,
9
+ placeholder,
10
+ class: className,
11
+ labelClass,
12
+ disabled,
13
+ readonly,
14
+ autocomplete,
15
+ mask,
16
+ tokens,
17
+ maskPlaceholder = '_',
18
+ showMaskPlaceholder = false,
19
+ unmaskValue = true,
20
+ showIssues,
21
+ prefix,
22
+ suffix,
23
+ wrapperClass,
24
+ onblur,
25
+ oninput
26
+ }: MaskedInputProps = $props();
27
+
28
+ // Resolve mask pattern
29
+ const resolvedMask = $derived(mask in MASK_PATTERNS ? MASK_PATTERNS[mask as MaskPattern] : mask);
30
+
31
+ // Get aria-invalid from field, respecting showIssues
32
+ const ariaInvalid = $derived(showIssues ? field.as('text')['aria-invalid'] : undefined);
33
+
34
+ let inputElement: HTMLInputElement | undefined = $state();
35
+
36
+ // Local display value state - synced with field
37
+ let displayValue = $state('');
38
+ let maskComplete = $state(false);
39
+
40
+ // Track the last value WE stored to distinguish our updates from external changes
41
+ let lastStoredValue: string | undefined = undefined;
42
+
43
+ // Initialize from field value - only for EXTERNAL changes (initial load, programmatic updates)
44
+ $effect(() => {
45
+ const fieldVal = field.value();
46
+ const val = typeof fieldVal === 'string' ? fieldVal : '';
47
+
48
+ // Only sync from field if this is NOT a value we just stored
49
+ // This prevents the $effect from fighting with user input
50
+ if (val === lastStoredValue) {
51
+ // This is our own update echoing back, ignore it
52
+ return;
53
+ }
54
+
55
+ if (val) {
56
+ const result = applyMask(val, {
57
+ mask: resolvedMask,
58
+ tokens,
59
+ placeholder: maskPlaceholder,
60
+ showPlaceholder: showMaskPlaceholder
61
+ });
62
+ displayValue = result.masked;
63
+ maskComplete = result.complete;
64
+ // Update lastStoredValue to match what we're displaying
65
+ lastStoredValue = unmaskValue ? result.raw : result.masked;
66
+ } else {
67
+ displayValue = '';
68
+ maskComplete = false;
69
+ lastStoredValue = '';
70
+ }
71
+ });
72
+
73
+ function processInput(
74
+ inputValue: string,
75
+ cursorPos: number
76
+ ): { masked: string; raw: string; cursorPosition: number; complete: boolean } {
77
+ const result = applyMask(
78
+ inputValue,
79
+ {
80
+ mask: resolvedMask,
81
+ tokens,
82
+ placeholder: maskPlaceholder,
83
+ showPlaceholder: showMaskPlaceholder
84
+ },
85
+ cursorPos
86
+ );
87
+
88
+ return result;
89
+ }
90
+
91
+ function handleInput(event: Event) {
92
+ const input = event.target as HTMLInputElement;
93
+ const cursorPos = input.selectionStart ?? 0;
94
+
95
+ const result = processInput(input.value, cursorPos);
96
+
97
+ // Update local state immediately
98
+ displayValue = result.masked;
99
+ maskComplete = result.complete;
100
+
101
+ // Store in field and track what we stored
102
+ const valueToStore = unmaskValue ? result.raw : result.masked;
103
+ lastStoredValue = valueToStore;
104
+ field.set(valueToStore);
105
+
106
+ // Restore cursor position after DOM update
107
+ requestAnimationFrame(() => {
108
+ if (inputElement) {
109
+ inputElement.setSelectionRange(result.cursorPosition, result.cursorPosition);
110
+ }
111
+ });
112
+
113
+ oninput?.();
114
+ }
115
+
116
+ function handlePaste(event: ClipboardEvent) {
117
+ event.preventDefault();
118
+ const input = event.target as HTMLInputElement;
119
+ const pastedText = event.clipboardData?.getData('text') ?? '';
120
+ const selStart = input.selectionStart ?? 0;
121
+ const selEnd = input.selectionEnd ?? 0;
122
+
123
+ // Build new value with pasted content
124
+ const before = displayValue.slice(0, selStart);
125
+ const after = displayValue.slice(selEnd);
126
+ const newValue = before + pastedText + after;
127
+
128
+ const result = processInput(newValue, selStart + pastedText.length);
129
+
130
+ displayValue = result.masked;
131
+ maskComplete = result.complete;
132
+
133
+ // Store and track the value
134
+ const valueToStore = unmaskValue ? result.raw : result.masked;
135
+ lastStoredValue = valueToStore;
136
+ field.set(valueToStore);
137
+
138
+ requestAnimationFrame(() => {
139
+ if (inputElement) {
140
+ inputElement.setSelectionRange(result.cursorPosition, result.cursorPosition);
141
+ }
142
+ });
143
+
144
+ oninput?.();
145
+ }
146
+
147
+ function handleKeyDown(event: KeyboardEvent) {
148
+ // Handle backspace specially to skip over literals
149
+ if (event.key === 'Backspace') {
150
+ const input = event.target as HTMLInputElement;
151
+ const cursorPos = input.selectionStart ?? 0;
152
+
153
+ if (cursorPos > 0 && input.selectionStart === input.selectionEnd) {
154
+ const charBefore = displayValue[cursorPos - 1];
155
+ const maskChar = resolvedMask[cursorPos - 1];
156
+
157
+ // If the character before cursor is a literal, skip it
158
+ if (charBefore === maskChar && !/[9aA*]/.test(maskChar)) {
159
+ event.preventDefault();
160
+ input.setSelectionRange(cursorPos - 1, cursorPos - 1);
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // Handle autocomplete/autofill by watching for changes
167
+ function handleChange(event: Event) {
168
+ const input = event.target as HTMLInputElement;
169
+
170
+ // Process the entire autofilled value
171
+ const result = processInput(input.value, input.value.length);
172
+
173
+ displayValue = result.masked;
174
+ maskComplete = result.complete;
175
+
176
+ // Store and track the value
177
+ const valueToStore = unmaskValue ? result.raw : result.masked;
178
+ lastStoredValue = valueToStore;
179
+ field.set(valueToStore);
180
+
181
+ oninput?.();
182
+ }
183
+ </script>
184
+
185
+ {#if label}
186
+ <label class={labelClass} for={name}>{label}</label>
187
+ {/if}
188
+ <!-- Hidden input holds the actual value for form submission -->
189
+ <input type="hidden" {name} value={field.value() ?? ''} />
190
+ <!-- Visible input shows masked display value but doesn't submit (no name) -->
191
+ <div class="sform-input-wrapper {wrapperClass ?? ''}">
192
+ {#if prefix}
193
+ <div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
194
+ {#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
195
+ </div>
196
+ {/if}
197
+ <input
198
+ bind:this={inputElement}
199
+ type="text"
200
+ id={name}
201
+ class={className}
202
+ {placeholder}
203
+ {disabled}
204
+ {readonly}
205
+ {autocomplete}
206
+ aria-invalid={ariaInvalid}
207
+ bind:value={displayValue}
208
+ oninput={handleInput}
209
+ onpaste={handlePaste}
210
+ onchange={handleChange}
211
+ onkeydown={handleKeyDown}
212
+ {onblur}
213
+ data-mask-complete={maskComplete}
214
+ />
215
+ {#if suffix}
216
+ <div class="sform-suffix" onclick={() => inputElement?.focus()} role="presentation">
217
+ {#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
218
+ </div>
219
+ {/if}
220
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { MaskedInputProps } from '../types.js';
2
+ declare const MaskedInput: import("svelte").Component<MaskedInputProps, {}, "">;
3
+ type MaskedInput = ReturnType<typeof MaskedInput>;
4
+ export default MaskedInput;