@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,116 @@
1
+ <script lang="ts">
2
+ import type { NumberInputComponentProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ placeholder,
9
+ class: className,
10
+ labelClass,
11
+ min,
12
+ max,
13
+ step,
14
+ disabled,
15
+ readonly,
16
+ autocomplete,
17
+ showIssues,
18
+ prefix,
19
+ suffix,
20
+ wrapperClass,
21
+ showControls = true,
22
+ align = 'start',
23
+ maxDecimals,
24
+ onblur,
25
+ oninput
26
+ }: NumberInputComponentProps = $props();
27
+
28
+ const fieldAttrs = $derived({
29
+ ...field.as('number'),
30
+ 'aria-invalid': showIssues ? field.as('number')['aria-invalid'] : undefined
31
+ });
32
+
33
+ const inputStyle = $derived(
34
+ [!showControls && 'appearance: textfield;', align === 'end' && 'text-align: end;']
35
+ .filter(Boolean)
36
+ .join(' ') || undefined
37
+ );
38
+
39
+ let inputElement: HTMLInputElement | undefined = $state();
40
+
41
+ // Handle keydown to prevent period when maxDecimals is 0
42
+ function handleKeyDown(event: KeyboardEvent) {
43
+ if (maxDecimals === 0 && (event.key === '.' || event.key === ',')) {
44
+ event.preventDefault();
45
+ }
46
+ }
47
+
48
+ // Handle input to enforce decimal limits
49
+ function handleInput(event: Event) {
50
+ if (maxDecimals === undefined) {
51
+ oninput?.();
52
+ return;
53
+ }
54
+
55
+ const input = event.target as HTMLInputElement;
56
+ const value = input.value;
57
+
58
+ // Find decimal separator (. or ,)
59
+ const decimalIndex = Math.max(value.indexOf('.'), value.indexOf(','));
60
+
61
+ if (decimalIndex !== -1) {
62
+ const decimals = value.length - decimalIndex - 1;
63
+ if (decimals > maxDecimals) {
64
+ // Truncate to maxDecimals
65
+ const truncated = value.slice(0, decimalIndex + maxDecimals + 1);
66
+ input.value = truncated;
67
+ // Also update the field value
68
+ field.set(parseFloat(truncated) || 0);
69
+ }
70
+ }
71
+
72
+ oninput?.();
73
+ }
74
+ </script>
75
+
76
+ {#if label}
77
+ <label class={labelClass} for={name}>{label}</label>
78
+ {/if}
79
+ <div class="sform-input-wrapper {wrapperClass ?? ''}">
80
+ {#if prefix}
81
+ <div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
82
+ {#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
83
+ </div>
84
+ {/if}
85
+ <input
86
+ bind:this={inputElement}
87
+ {...fieldAttrs}
88
+ id={name}
89
+ class="{className ?? ''}{showControls ? '' : ' sform-number-no-controls'}"
90
+ style={inputStyle}
91
+ {placeholder}
92
+ {min}
93
+ {max}
94
+ {step}
95
+ {disabled}
96
+ {readonly}
97
+ {autocomplete}
98
+ {onblur}
99
+ onkeydown={handleKeyDown}
100
+ oninput={handleInput}
101
+ />
102
+ {#if suffix}
103
+ <div class="sform-suffix" onclick={() => inputElement?.focus()} role="presentation">
104
+ {#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
105
+ </div>
106
+ {/if}
107
+ </div>
108
+
109
+ <style>
110
+ /* Hide spinbuttons when showControls is false */
111
+ input.sform-number-no-controls::-webkit-outer-spin-button,
112
+ input.sform-number-no-controls::-webkit-inner-spin-button {
113
+ -webkit-appearance: none;
114
+ margin: 0;
115
+ }
116
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { NumberInputComponentProps } from '../types.js';
2
+ declare const NumberInput: import("svelte").Component<NumberInputComponentProps, {}, "">;
3
+ type NumberInput = ReturnType<typeof NumberInput>;
4
+ export default NumberInput;
@@ -0,0 +1,142 @@
1
+ <script lang="ts">
2
+ import type { PasswordInputProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ placeholder,
9
+ class: className,
10
+ labelClass,
11
+ disabled,
12
+ readonly,
13
+ autocomplete,
14
+ showToggle = true,
15
+ showIssues,
16
+ onblur,
17
+ oninput,
18
+ showToggleIcon
19
+ }: PasswordInputProps = $props();
20
+
21
+ let showPassword = $state(false);
22
+
23
+ const fieldAttrs = $derived({
24
+ ...field.as(showPassword ? 'text' : 'password'),
25
+ 'aria-invalid': showIssues
26
+ ? field.as(showPassword ? 'text' : 'password')['aria-invalid']
27
+ : undefined
28
+ });
29
+
30
+ function toggleVisibility() {
31
+ showPassword = !showPassword;
32
+ }
33
+ </script>
34
+
35
+ {#if label}
36
+ <label class={labelClass} for={name}>{label}</label>
37
+ {/if}
38
+ <div class="sform-password-wrapper">
39
+ <input
40
+ {...fieldAttrs}
41
+ type={showPassword ? 'text' : 'password'}
42
+ id={name}
43
+ class={className}
44
+ {placeholder}
45
+ {disabled}
46
+ {readonly}
47
+ {autocomplete}
48
+ {onblur}
49
+ {oninput}
50
+ />
51
+ {#if showToggle}
52
+ <button
53
+ type="button"
54
+ class="sform-password-toggle"
55
+ onclick={toggleVisibility}
56
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
57
+ tabindex={-1}
58
+ {disabled}
59
+ >
60
+ {#if showToggleIcon !== undefined}
61
+ {@render showToggleIcon(showPassword)}
62
+ {:else if showPassword}
63
+ <!-- Eye closed icon (password visible) -->
64
+ <svg
65
+ xmlns="http://www.w3.org/2000/svg"
66
+ width="20"
67
+ height="20"
68
+ viewBox="0 0 24 24"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ stroke-width="2"
72
+ stroke-linecap="round"
73
+ stroke-linejoin="round"
74
+ >
75
+ <path
76
+ d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
77
+ ></path>
78
+ <line x1="1" y1="1" x2="23" y2="23"></line>
79
+ </svg>
80
+ {:else}
81
+ <!-- Eye open icon (password hidden) -->
82
+ <svg
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ width="20"
85
+ height="20"
86
+ viewBox="0 0 24 24"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ stroke-width="2"
90
+ stroke-linecap="round"
91
+ stroke-linejoin="round"
92
+ >
93
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
94
+ <circle cx="12" cy="12" r="3"></circle>
95
+ </svg>
96
+ {/if}
97
+ </button>
98
+ {/if}
99
+ </div>
100
+
101
+ <style>
102
+ .sform-password-wrapper {
103
+ position: relative;
104
+ display: inline-flex;
105
+ align-items: center;
106
+ width: 100%;
107
+ }
108
+
109
+ .sform-password-wrapper input {
110
+ width: 100%;
111
+ padding-right: 2.5rem;
112
+ }
113
+
114
+ .sform-password-toggle {
115
+ position: absolute;
116
+ right: 0.5rem;
117
+ background: none;
118
+ border: none;
119
+ cursor: pointer;
120
+ padding: 0.25rem;
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ color: #666;
125
+ border-radius: 0.25rem;
126
+ }
127
+
128
+ .sform-password-toggle:hover:not(:disabled) {
129
+ color: #333;
130
+ background-color: rgba(0, 0, 0, 0.05);
131
+ }
132
+
133
+ .sform-password-toggle:disabled {
134
+ opacity: 0.5;
135
+ cursor: not-allowed;
136
+ }
137
+
138
+ .sform-password-toggle:focus {
139
+ outline: 2px solid #2196f3;
140
+ outline-offset: 2px;
141
+ }
142
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { PasswordInputProps } from '../types.js';
2
+ declare const PasswordInput: import("svelte").Component<PasswordInputProps, {}, "">;
3
+ type PasswordInput = ReturnType<typeof PasswordInput>;
4
+ export default PasswordInput;
@@ -0,0 +1,81 @@
1
+ <script lang="ts">
2
+ import type { RadioInputProps, 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
+ }: RadioInputProps = $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('radio', optionValue);
25
+ return {
26
+ ...attrs,
27
+ 'aria-invalid': showIssues ? attrs['aria-invalid'] : undefined
28
+ };
29
+ }
30
+ </script>
31
+
32
+ <fieldset class="sform-radio-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-radio-option" class:disabled={option.disabled}>
40
+ <input
41
+ {...fieldAttrs}
42
+ type="radio"
43
+ id={uniqueId}
44
+ disabled={disabled || option.disabled}
45
+ {onblur}
46
+ {oninput}
47
+ />
48
+ <span class="sform-radio-label">{option.label}</span>
49
+ </label>
50
+ {/each}
51
+ </fieldset>
52
+
53
+ <style>
54
+ .sform-radio-group {
55
+ border: none;
56
+ padding: 0;
57
+ margin: 0;
58
+ }
59
+
60
+ .sform-radio-group legend {
61
+ padding: 0;
62
+ margin-bottom: 0.5rem;
63
+ }
64
+
65
+ .sform-radio-option {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.5rem;
69
+ cursor: pointer;
70
+ padding: 0.25rem 0;
71
+ }
72
+
73
+ .sform-radio-option.disabled {
74
+ opacity: 0.6;
75
+ cursor: not-allowed;
76
+ }
77
+
78
+ .sform-radio-option input {
79
+ margin: 0;
80
+ }
81
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { RadioInputProps } from '../types.js';
2
+ declare const RadioInput: import("svelte").Component<RadioInputProps, {}, "">;
3
+ type RadioInput = ReturnType<typeof RadioInput>;
4
+ export default RadioInput;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import type { RangeInputProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ class: className,
9
+ labelClass,
10
+ min = 0,
11
+ max = 100,
12
+ step = 1,
13
+ disabled,
14
+ showValue = true,
15
+ formatValue = (v: number) => String(v),
16
+ showIssues,
17
+ onblur,
18
+ oninput
19
+ }: RangeInputProps = $props();
20
+
21
+ const fieldAttrs = $derived({
22
+ ...field.as('range'),
23
+ 'aria-invalid': showIssues ? field.as('range')['aria-invalid'] : undefined
24
+ });
25
+
26
+ // Get current value for display
27
+ const currentValue = $derived.by(() => {
28
+ const val = field.value();
29
+ return typeof val === 'number' ? val : Number(val) || Number(min);
30
+ });
31
+ </script>
32
+
33
+ {#if label}
34
+ <label class={labelClass} for={name}>{label}</label>
35
+ {/if}
36
+ <div class="sform-range-wrapper">
37
+ <input
38
+ {...fieldAttrs}
39
+ type="range"
40
+ id={name}
41
+ class={className}
42
+ {min}
43
+ {max}
44
+ {step}
45
+ {disabled}
46
+ {onblur}
47
+ {oninput}
48
+ />
49
+ {#if showValue}
50
+ <output for={name} class="sform-range-value">
51
+ {formatValue(currentValue)}
52
+ </output>
53
+ {/if}
54
+ </div>
55
+
56
+ <style>
57
+ .sform-range-wrapper {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 0.5rem;
61
+ }
62
+
63
+ .sform-range-value {
64
+ min-width: 3ch;
65
+ text-align: right;
66
+ }
67
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { RangeInputProps } from '../types.js';
2
+ declare const RangeInput: import("svelte").Component<RangeInputProps, {}, "">;
3
+ type RangeInput = ReturnType<typeof RangeInput>;
4
+ export default RangeInput;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { SelectInputProps, SelectOption } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ class: className,
9
+ labelClass,
10
+ disabled,
11
+ showIssues,
12
+ onblur,
13
+ oninput,
14
+ options
15
+ }: SelectInputProps = $props();
16
+
17
+ const fieldAttrs = $derived({
18
+ ...field.as('select'),
19
+ 'aria-invalid': showIssues ? field.as('select')['aria-invalid'] : undefined
20
+ });
21
+
22
+ const normalizedOptions: SelectOption[] = $derived(
23
+ options.map((opt) => (typeof opt === 'string' ? { value: opt, label: opt } : opt))
24
+ );
25
+ </script>
26
+
27
+ {#if label}
28
+ <label class={labelClass} for={name}>{label}</label>
29
+ {/if}
30
+ <select {...fieldAttrs} id={name} class={className} {disabled} {onblur} {oninput}>
31
+ {#each normalizedOptions as option (option.value)}
32
+ <option value={option.value} disabled={option.disabled}>
33
+ {option.label}
34
+ </option>
35
+ {/each}
36
+ </select>
@@ -0,0 +1,4 @@
1
+ import type { SelectInputProps } from '../types.js';
2
+ declare const SelectInput: import("svelte").Component<SelectInputProps, {}, "">;
3
+ type SelectInput = ReturnType<typeof SelectInput>;
4
+ export default SelectInput;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { TextInputComponentProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ type,
7
+ name,
8
+ label,
9
+ placeholder,
10
+ class: className,
11
+ labelClass,
12
+ disabled,
13
+ readonly,
14
+ autocomplete,
15
+ showIssues,
16
+ prefix,
17
+ suffix,
18
+ wrapperClass,
19
+ onblur,
20
+ oninput
21
+ }: TextInputComponentProps = $props();
22
+
23
+ const fieldAttrs = $derived({
24
+ ...field.as(type as 'text'),
25
+ 'aria-invalid': showIssues ? field.as(type as 'text')['aria-invalid'] : undefined
26
+ });
27
+
28
+ let inputElement: HTMLInputElement | undefined = $state();
29
+ </script>
30
+
31
+ {#if type !== 'hidden'}
32
+ {#if label}
33
+ <label class={labelClass} for={name}>{label}</label>
34
+ {/if}
35
+ <div class="sform-input-wrapper {wrapperClass ?? ''}">
36
+ {#if prefix}
37
+ <div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
38
+ {#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
39
+ </div>
40
+ {/if}
41
+ <input
42
+ bind:this={inputElement}
43
+ {...fieldAttrs}
44
+ id={name}
45
+ class={className}
46
+ {placeholder}
47
+ {disabled}
48
+ {readonly}
49
+ {autocomplete}
50
+ {onblur}
51
+ {oninput}
52
+ />
53
+ {#if suffix}
54
+ <div class="sform-suffix" onclick={() => inputElement?.focus()} role="presentation">
55
+ {#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
56
+ </div>
57
+ {/if}
58
+ </div>
59
+ {:else}
60
+ <input {...fieldAttrs} type="hidden" id={name} />
61
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { TextInputComponentProps } from '../types.js';
2
+ declare const TextInput: import("svelte").Component<TextInputComponentProps, {}, "">;
3
+ type TextInput = ReturnType<typeof TextInput>;
4
+ export default TextInput;
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import type { TextareaInputProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ placeholder,
9
+ class: className,
10
+ labelClass,
11
+ disabled,
12
+ readonly,
13
+ autocomplete,
14
+ showIssues,
15
+ prefix,
16
+ suffix,
17
+ wrapperClass,
18
+ onblur,
19
+ oninput
20
+ }: TextareaInputProps = $props();
21
+
22
+ const fieldAttrs = $derived({
23
+ ...field.as('text'),
24
+ 'aria-invalid': showIssues ? field.as('text')['aria-invalid'] : undefined
25
+ });
26
+
27
+ let textareaElement: HTMLTextAreaElement | undefined = $state();
28
+ </script>
29
+
30
+ {#if label}
31
+ <label class={labelClass} for={name}>{label}</label>
32
+ {/if}
33
+ <div class="sform-input-wrapper sform-textarea-wrapper {wrapperClass ?? ''}">
34
+ {#if prefix}
35
+ <div class="sform-prefix" onclick={() => textareaElement?.focus()} role="presentation">
36
+ {#if typeof prefix === 'function'}{@render prefix()}{:else}{prefix}{/if}
37
+ </div>
38
+ {/if}
39
+ <textarea
40
+ bind:this={textareaElement}
41
+ {...fieldAttrs}
42
+ id={name}
43
+ class={className}
44
+ {placeholder}
45
+ {disabled}
46
+ {readonly}
47
+ {autocomplete}
48
+ {onblur}
49
+ {oninput}
50
+ ></textarea>
51
+ {#if suffix}
52
+ <div class="sform-suffix" onclick={() => textareaElement?.focus()} role="presentation">
53
+ {#if typeof suffix === 'function'}{@render suffix()}{:else}{suffix}{/if}
54
+ </div>
55
+ {/if}
56
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { TextareaInputProps } from '../types.js';
2
+ declare const TextareaInput: import("svelte").Component<TextareaInputProps, {}, "">;
3
+ type TextareaInput = ReturnType<typeof TextareaInput>;
4
+ export default TextareaInput;
@@ -0,0 +1,101 @@
1
+ <script lang="ts">
2
+ import type { ToggleInputProps } from '../types.js';
3
+
4
+ let {
5
+ field,
6
+ name,
7
+ label,
8
+ class: className,
9
+ labelClass,
10
+ disabled,
11
+ onLabel = 'On',
12
+ offLabel = 'Off',
13
+ checkedValue = 'true',
14
+ uncheckedValue = 'false',
15
+ onblur,
16
+ oninput
17
+ }: ToggleInputProps = $props();
18
+
19
+ const fieldAttrs = $derived(field.as('checkbox', checkedValue));
20
+
21
+ const isChecked = $derived.by(() => {
22
+ const val = field.value();
23
+ return val === checkedValue || val === true;
24
+ });
25
+ </script>
26
+
27
+ {#if label}
28
+ <span id="{name}-label" class={labelClass}>{label}</span>
29
+ {/if}
30
+ <label class="sform-toggle {className ?? ''}" class:disabled>
31
+ <input
32
+ {...fieldAttrs}
33
+ type="checkbox"
34
+ id={name}
35
+ aria-labelledby={label ? `${name}-label` : undefined}
36
+ {disabled}
37
+ value={checkedValue}
38
+ {onblur}
39
+ {oninput}
40
+ />
41
+ <span class="sform-toggle-track">
42
+ <span class="sform-toggle-thumb"></span>
43
+ </span>
44
+ <span class="sform-toggle-label">
45
+ {isChecked ? onLabel : offLabel}
46
+ </span>
47
+ </label>
48
+
49
+ <style>
50
+ .sform-toggle {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 0.5rem;
54
+ cursor: pointer;
55
+ }
56
+
57
+ .sform-toggle.disabled {
58
+ opacity: 0.5;
59
+ cursor: not-allowed;
60
+ }
61
+
62
+ .sform-toggle input {
63
+ position: absolute;
64
+ opacity: 0;
65
+ width: 0;
66
+ height: 0;
67
+ }
68
+
69
+ .sform-toggle-track {
70
+ position: relative;
71
+ width: 2.5rem;
72
+ height: 1.25rem;
73
+ background-color: #ccc;
74
+ border-radius: 1rem;
75
+ transition: background-color 0.2s;
76
+ }
77
+
78
+ .sform-toggle input:checked + .sform-toggle-track {
79
+ background-color: #4caf50;
80
+ }
81
+
82
+ .sform-toggle-thumb {
83
+ position: absolute;
84
+ top: 0.125rem;
85
+ left: 0.125rem;
86
+ width: 1rem;
87
+ height: 1rem;
88
+ background-color: white;
89
+ border-radius: 50%;
90
+ transition: transform 0.2s;
91
+ }
92
+
93
+ .sform-toggle input:checked + .sform-toggle-track .sform-toggle-thumb {
94
+ transform: translateX(1.25rem);
95
+ }
96
+
97
+ .sform-toggle input:focus + .sform-toggle-track {
98
+ outline: 2px solid #2196f3;
99
+ outline-offset: 2px;
100
+ }
101
+ </style>