@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.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/dist/Sform/Sfield.svelte +164 -0
- package/dist/Sform/Sfield.svelte.d.ts +4 -0
- package/dist/Sform/Sform.svelte +101 -0
- package/dist/Sform/Sform.svelte.d.ts +51 -0
- package/dist/Sform/context.svelte.d.ts +3 -0
- package/dist/Sform/context.svelte.js +65 -0
- package/dist/Sform/index.d.ts +5 -0
- package/dist/Sform/index.js +7 -0
- package/dist/Sform/inputs/ButtonInput.svelte +75 -0
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +32 -0
- package/dist/Sform/inputs/CheckboxGroupInput.svelte +81 -0
- package/dist/Sform/inputs/CheckboxGroupInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/CheckboxInput.svelte +47 -0
- package/dist/Sform/inputs/CheckboxInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/MaskedInput.svelte +220 -0
- package/dist/Sform/inputs/MaskedInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/NumberInput.svelte +116 -0
- package/dist/Sform/inputs/NumberInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/PasswordInput.svelte +142 -0
- package/dist/Sform/inputs/PasswordInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/RadioInput.svelte +81 -0
- package/dist/Sform/inputs/RadioInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/RangeInput.svelte +67 -0
- package/dist/Sform/inputs/RangeInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/SelectInput.svelte +36 -0
- package/dist/Sform/inputs/SelectInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/TextInput.svelte +61 -0
- package/dist/Sform/inputs/TextInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/TextareaInput.svelte +56 -0
- package/dist/Sform/inputs/TextareaInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/ToggleInput.svelte +101 -0
- package/dist/Sform/inputs/ToggleInput.svelte.d.ts +4 -0
- package/dist/Sform/inputs/ToggleOptionsInput.svelte +118 -0
- package/dist/Sform/inputs/ToggleOptionsInput.svelte.d.ts +4 -0
- package/dist/Sform/sform.css +628 -0
- package/dist/Sform/types.d.ts +610 -0
- package/dist/Sform/types.js +5 -0
- package/dist/Sform/utils/mask.d.ts +68 -0
- package/dist/Sform/utils/mask.js +155 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- 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,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,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,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,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,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,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,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>
|