@samuel-charpentier/sform 0.0.11 → 1.0.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 +0 -34
- package/dist/Sform/Sfield.svelte +0 -3
- package/dist/Sform/Sform.svelte +0 -1
- package/dist/Sform/inputs/ButtonInput.svelte +6 -7
- package/dist/Sform/inputs/ButtonInput.svelte.d.ts +2 -4
- package/dist/Sform/inputs/CheckboxGroupInput.svelte +3 -31
- package/dist/Sform/inputs/CheckboxInput.svelte +5 -1
- package/dist/Sform/inputs/MaskedInput.svelte +2 -3
- package/dist/Sform/inputs/NumberInput.svelte +2 -3
- package/dist/Sform/inputs/PasswordInput.svelte +2 -3
- package/dist/Sform/inputs/RadioCheckboxLabel.svelte +50 -0
- package/dist/Sform/inputs/RadioCheckboxLabel.svelte.d.ts +39 -0
- package/dist/Sform/inputs/RadioInput.svelte +3 -42
- package/dist/Sform/inputs/RangeInput.svelte +2 -3
- package/dist/Sform/inputs/SelectInput.svelte +2 -3
- package/dist/Sform/inputs/TextInput.svelte +2 -3
- package/dist/Sform/inputs/TextareaInput.svelte +2 -3
- package/dist/Sform/inputs/ToggleInput.svelte +5 -1
- package/dist/Sform/inputs/TopLabel.svelte +14 -0
- package/dist/Sform/inputs/TopLabel.svelte.d.ts +4 -0
- package/dist/Sform/types.d.ts +11 -10
- package/dist/Sform/utils/Fieldset.svelte +8 -3
- package/package.json +1 -1
- package/dist/Sform/inputs/ToggleOptionsInput.svelte +0 -118
- package/dist/Sform/inputs/ToggleOptionsInput.svelte.d.ts +0 -4
package/README.md
CHANGED
|
@@ -23,7 +23,6 @@ A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
|
23
23
|
- [Radio](#radio)
|
|
24
24
|
- [Range](#range)
|
|
25
25
|
- [Toggle](#toggle)
|
|
26
|
-
- [Toggle Options](#toggle-options)
|
|
27
26
|
- [Masked Input](#masked-input)
|
|
28
27
|
- [Hidden Input](#hidden-input)
|
|
29
28
|
- [`<Sbutton>`](#sbutton)
|
|
@@ -44,7 +43,6 @@ A type-safe form library for **Svelte 5** with **SvelteKit remote functions**.
|
|
|
44
43
|
- ✅ **Masked inputs** - Phone, credit card, SSN formatting
|
|
45
44
|
- ✅ **Range slider** - With optional value display
|
|
46
45
|
- ✅ **Toggle switch** - Modern on/off control
|
|
47
|
-
- ✅ **Toggle options** - Segmented control for mutually exclusive options
|
|
48
46
|
- ✅ **Stateful button** - Shows pending state during submission
|
|
49
47
|
|
|
50
48
|
## Requirements
|
|
@@ -311,38 +309,6 @@ Supported text types: `text`, `email`, `tel`, `url`, `search`, `date`, `datetime
|
|
|
311
309
|
| `checkedValue` | `string` | `'true'` | Value when checked |
|
|
312
310
|
| `uncheckedValue` | `string` | `'false'` | Value when unchecked |
|
|
313
311
|
|
|
314
|
-
#### Toggle Options
|
|
315
|
-
|
|
316
|
-
```svelte
|
|
317
|
-
<Sfield
|
|
318
|
-
field={fields.theme}
|
|
319
|
-
type="toggle-options"
|
|
320
|
-
label="Theme"
|
|
321
|
-
options={[
|
|
322
|
-
{ value: 'light', label: 'Light' },
|
|
323
|
-
{ value: 'dark', label: 'Dark' },
|
|
324
|
-
{ value: 'auto', label: 'Auto' }
|
|
325
|
-
]}
|
|
326
|
-
/>
|
|
327
|
-
<!-- Multiple selection -->
|
|
328
|
-
<Sfield
|
|
329
|
-
field={fields.features}
|
|
330
|
-
type="toggle-options"
|
|
331
|
-
label="Features"
|
|
332
|
-
multiple={true}
|
|
333
|
-
options={[
|
|
334
|
-
{ value: 'push', label: 'Push Notifications' },
|
|
335
|
-
{ value: 'email', label: 'Email' },
|
|
336
|
-
{ value: 'sms', label: 'SMS' }
|
|
337
|
-
]}
|
|
338
|
-
/>
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
| Prop | Type | Default | Description |
|
|
342
|
-
| ---------- | ---------------------------- | -------- | ------------------------- |
|
|
343
|
-
| `options` | `ToggleOption[] \| string[]` | required | Toggle options |
|
|
344
|
-
| `multiple` | `boolean` | `false` | Allow multiple selections |
|
|
345
|
-
|
|
346
312
|
#### Masked Input
|
|
347
313
|
|
|
348
314
|
```svelte
|
package/dist/Sform/Sfield.svelte
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
import RadioInput from './inputs/RadioInput.svelte';
|
|
16
16
|
import RangeInput from './inputs/RangeInput.svelte';
|
|
17
17
|
import ToggleInput from './inputs/ToggleInput.svelte';
|
|
18
|
-
import ToggleOptionsInput from './inputs/ToggleOptionsInput.svelte';
|
|
19
18
|
import MaskedInput from './inputs/MaskedInput.svelte';
|
|
20
19
|
import PasswordInput from './inputs/PasswordInput.svelte';
|
|
21
20
|
import HiddenInput from './inputs/HiddenInput.svelte';
|
|
@@ -147,8 +146,6 @@
|
|
|
147
146
|
<RangeInput {...passthroughProps()} {...internalProps} />
|
|
148
147
|
{:else if props.type === 'toggle'}
|
|
149
148
|
<ToggleInput {...passthroughProps()} {...internalProps} />
|
|
150
|
-
{:else if props.type === 'toggle-options'}
|
|
151
|
-
<ToggleOptionsInput {...passthroughProps()} {...internalProps} />
|
|
152
149
|
{:else if props.type === 'masked'}
|
|
153
150
|
<MaskedInput {...passthroughProps()} {...internalProps} />
|
|
154
151
|
{:else if props.type === 'hidden'}
|
package/dist/Sform/Sform.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="T = unknown">
|
|
2
|
-
import type
|
|
2
|
+
import { tick, type Snippet } from 'svelte';
|
|
3
3
|
import type { ButtonState, RemoteFormIssue } from '../types.js';
|
|
4
4
|
import { getSformContext } from '../context.svelte.js';
|
|
5
5
|
|
|
@@ -20,15 +20,13 @@
|
|
|
20
20
|
/** The remote form - used to infer the result type T */
|
|
21
21
|
form: FormLike<T>;
|
|
22
22
|
/** Button text (used if no children snippet provided) */
|
|
23
|
-
label?: string
|
|
23
|
+
label?: string | Snippet<[ButtonState<T>]>;
|
|
24
24
|
/** Button type */
|
|
25
25
|
buttonType?: 'submit' | 'reset' | 'button';
|
|
26
26
|
/** Button class */
|
|
27
27
|
class?: string;
|
|
28
28
|
/** Whether button is disabled */
|
|
29
29
|
disabled?: boolean;
|
|
30
|
-
/** Children snippet receives ButtonState<T> for custom rendering with typed result */
|
|
31
|
-
children?: Snippet<[ButtonState<T>]>;
|
|
32
30
|
/** Callback that runs before validation/submission (can be async) */
|
|
33
31
|
onsubmit?: () => void | Promise<void>;
|
|
34
32
|
}
|
|
@@ -38,7 +36,6 @@
|
|
|
38
36
|
buttonType = 'submit',
|
|
39
37
|
class: className,
|
|
40
38
|
disabled = false,
|
|
41
|
-
children,
|
|
42
39
|
onsubmit
|
|
43
40
|
}: Props = $props();
|
|
44
41
|
|
|
@@ -59,6 +56,8 @@
|
|
|
59
56
|
// This ensures blur validation runs with valid form data, not stale data
|
|
60
57
|
buttonElement.focus();
|
|
61
58
|
|
|
59
|
+
await tick();
|
|
60
|
+
|
|
62
61
|
if (onsubmit) {
|
|
63
62
|
await onsubmit();
|
|
64
63
|
}
|
|
@@ -80,8 +79,8 @@
|
|
|
80
79
|
disabled={isDisabled}
|
|
81
80
|
onclick={handleClick}
|
|
82
81
|
>
|
|
83
|
-
{#if
|
|
84
|
-
{@render
|
|
82
|
+
{#if typeof label === 'function'}
|
|
83
|
+
{@render label(formState)}
|
|
85
84
|
{:else}
|
|
86
85
|
{label}
|
|
87
86
|
{/if}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
2
|
import type { ButtonState, RemoteFormIssue } from '../types.js';
|
|
3
3
|
declare function $$render<T = unknown>(): {
|
|
4
4
|
props: {
|
|
@@ -12,15 +12,13 @@ declare function $$render<T = unknown>(): {
|
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
14
|
/** Button text (used if no children snippet provided) */
|
|
15
|
-
label?: string
|
|
15
|
+
label?: string | Snippet<[ButtonState<T>]>;
|
|
16
16
|
/** Button type */
|
|
17
17
|
buttonType?: "submit" | "reset" | "button";
|
|
18
18
|
/** Button class */
|
|
19
19
|
class?: string;
|
|
20
20
|
/** Whether button is disabled */
|
|
21
21
|
disabled?: boolean;
|
|
22
|
-
/** Children snippet receives ButtonState<T> for custom rendering with typed result */
|
|
23
|
-
children?: Snippet<[ButtonState<T>]>;
|
|
24
22
|
/** Callback that runs before validation/submission (can be async) */
|
|
25
23
|
onsubmit?: () => void | Promise<void>;
|
|
26
24
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CheckboxGroupInputProps, SelectOption } from '../types.js';
|
|
3
3
|
import Fieldset from '../utils/Fieldset.svelte';
|
|
4
|
+
import RadioCheckboxLabel from './RadioCheckboxLabel.svelte';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
field,
|
|
@@ -30,39 +31,10 @@
|
|
|
30
31
|
}
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
|
-
<Fieldset {label} {labelClass} {className} {disabled}>
|
|
34
|
+
<Fieldset {label} {labelClass} {className} {disabled} type="checkbox">
|
|
34
35
|
{#each normalizedOptions as option}
|
|
35
36
|
{@const fieldAttrs = getFieldAttrs(option.value)}
|
|
36
37
|
{@const uniqueId = `${name}-${option.value}`}
|
|
37
|
-
<
|
|
38
|
-
<input
|
|
39
|
-
{...fieldAttrs}
|
|
40
|
-
type="checkbox"
|
|
41
|
-
id={uniqueId}
|
|
42
|
-
disabled={disabled || option.disabled}
|
|
43
|
-
{onblur}
|
|
44
|
-
{oninput}
|
|
45
|
-
/>
|
|
46
|
-
<span class="sform-checkbox-label">{option.label}</span>
|
|
47
|
-
</label>
|
|
38
|
+
<RadioCheckboxLabel {fieldAttrs} {uniqueId} {option} {disabled} {onblur} {oninput} />
|
|
48
39
|
{/each}
|
|
49
40
|
</Fieldset>
|
|
50
|
-
|
|
51
|
-
<style>
|
|
52
|
-
.sform-checkbox-option {
|
|
53
|
-
display: flex;
|
|
54
|
-
align-items: center;
|
|
55
|
-
gap: 0.5rem;
|
|
56
|
-
cursor: pointer;
|
|
57
|
-
padding: 0.25rem 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.sform-checkbox-option.disabled {
|
|
61
|
-
opacity: 0.6;
|
|
62
|
-
cursor: not-allowed;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.sform-checkbox-option input {
|
|
66
|
-
margin: 0;
|
|
67
|
-
}
|
|
68
|
-
</style>
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
<label class="sform-checkbox {className ?? ''}" class:disabled>
|
|
25
25
|
<input {...fieldAttrs} type="checkbox" id={name} {disabled} {onblur} {oninput} />
|
|
26
26
|
{#if label}
|
|
27
|
-
|
|
27
|
+
{#if typeof label === 'function'}
|
|
28
|
+
{@render label()}
|
|
29
|
+
{:else}
|
|
30
|
+
<span class={labelClass}>{label}</span>
|
|
31
|
+
{/if}
|
|
28
32
|
{/if}
|
|
29
33
|
</label>
|
|
30
34
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { MaskedInputProps } from '../types.js';
|
|
3
3
|
import { applyMask, MASK_PATTERNS, type MaskPattern, type MaskToken } from '../utils/mask.js';
|
|
4
|
+
import TopLabel from './TopLabel.svelte';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
field,
|
|
@@ -182,9 +183,7 @@
|
|
|
182
183
|
}
|
|
183
184
|
</script>
|
|
184
185
|
|
|
185
|
-
{
|
|
186
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
187
|
-
{/if}
|
|
186
|
+
<TopLabel {label} {labelClass} {name} />
|
|
188
187
|
<!-- Hidden input holds the actual value for form submission -->
|
|
189
188
|
<input type="hidden" {name} value={field.value() ?? ''} />
|
|
190
189
|
<!-- Visible input shows masked display value but doesn't submit (no name) -->
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { NumberInputComponentProps } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -73,9 +74,7 @@
|
|
|
73
74
|
}
|
|
74
75
|
</script>
|
|
75
76
|
|
|
76
|
-
{
|
|
77
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
78
|
-
{/if}
|
|
77
|
+
<TopLabel {label} {labelClass} {name} />
|
|
79
78
|
<div class="sform-input-wrapper {wrapperClass ?? ''}">
|
|
80
79
|
{#if prefix}
|
|
81
80
|
<div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PasswordInputProps } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -32,9 +33,7 @@
|
|
|
32
33
|
}
|
|
33
34
|
</script>
|
|
34
35
|
|
|
35
|
-
{
|
|
36
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
37
|
-
{/if}
|
|
36
|
+
<TopLabel {label} {labelClass} {name} />
|
|
38
37
|
<div class="sform-password-wrapper">
|
|
39
38
|
<input
|
|
40
39
|
{...fieldAttrs}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts" generics="InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')">
|
|
2
|
+
import type { HTMLInputTypeAttribute } from 'svelte/elements';
|
|
3
|
+
import type { SelectOption } from '../types';
|
|
4
|
+
|
|
5
|
+
type FieldAttrs<InputType> = {
|
|
6
|
+
'aria-invalid'?: boolean | 'false' | 'true';
|
|
7
|
+
name: string;
|
|
8
|
+
type: InputType;
|
|
9
|
+
value?: string;
|
|
10
|
+
checked: boolean;
|
|
11
|
+
};
|
|
12
|
+
type Props = {
|
|
13
|
+
fieldAttrs: FieldAttrs<InputType>;
|
|
14
|
+
uniqueId: string;
|
|
15
|
+
option: SelectOption;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
onblur?: (event: FocusEvent) => void;
|
|
18
|
+
oninput?: (event: Event) => void;
|
|
19
|
+
};
|
|
20
|
+
let { fieldAttrs, uniqueId, option, disabled, onblur, oninput }: Props = $props();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<label class={`sform-${fieldAttrs.type}-option`} class:disabled={option.disabled}>
|
|
24
|
+
<input {...fieldAttrs} id={uniqueId} disabled={disabled || option.disabled} {onblur} {oninput} />
|
|
25
|
+
{#if typeof option.label === 'function'}
|
|
26
|
+
{@render option.label(option)}
|
|
27
|
+
{:else}
|
|
28
|
+
<span class={`sform-${fieldAttrs.type}-label`}>{option.label}</span>
|
|
29
|
+
{/if}
|
|
30
|
+
</label>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.sform-checkbox-option,
|
|
34
|
+
.sform-radio-option {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.5rem;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
padding: 0.25rem 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:is(.sform-checkbox-option, .sform-radio-option).disabled {
|
|
43
|
+
opacity: 0.6;
|
|
44
|
+
cursor: not-allowed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:is(.sform-checkbox-option, .sform-radio-option) input {
|
|
48
|
+
margin: 0;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HTMLInputTypeAttribute } from 'svelte/elements';
|
|
2
|
+
import type { SelectOption } from '../types';
|
|
3
|
+
declare function $$render<InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')>(): {
|
|
4
|
+
props: {
|
|
5
|
+
fieldAttrs: {
|
|
6
|
+
'aria-invalid'?: boolean | "false" | "true";
|
|
7
|
+
name: string;
|
|
8
|
+
type: InputType;
|
|
9
|
+
value?: string;
|
|
10
|
+
checked: boolean;
|
|
11
|
+
};
|
|
12
|
+
uniqueId: string;
|
|
13
|
+
option: SelectOption;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onblur?: (event: FocusEvent) => void;
|
|
16
|
+
oninput?: (event: Event) => void;
|
|
17
|
+
};
|
|
18
|
+
exports: {};
|
|
19
|
+
bindings: "";
|
|
20
|
+
slots: {};
|
|
21
|
+
events: {};
|
|
22
|
+
};
|
|
23
|
+
declare class __sveltets_Render<InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')> {
|
|
24
|
+
props(): ReturnType<typeof $$render<InputType>>['props'];
|
|
25
|
+
events(): ReturnType<typeof $$render<InputType>>['events'];
|
|
26
|
+
slots(): ReturnType<typeof $$render<InputType>>['slots'];
|
|
27
|
+
bindings(): "";
|
|
28
|
+
exports(): {};
|
|
29
|
+
}
|
|
30
|
+
interface $$IsomorphicComponent {
|
|
31
|
+
new <InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<InputType>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<InputType>['props']>, ReturnType<__sveltets_Render<InputType>['events']>, ReturnType<__sveltets_Render<InputType>['slots']>> & {
|
|
32
|
+
$$bindings?: ReturnType<__sveltets_Render<InputType>['bindings']>;
|
|
33
|
+
} & ReturnType<__sveltets_Render<InputType>['exports']>;
|
|
34
|
+
<InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')>(internal: unknown, props: ReturnType<__sveltets_Render<InputType>['props']> & {}): ReturnType<__sveltets_Render<InputType>['exports']>;
|
|
35
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
36
|
+
}
|
|
37
|
+
declare const RadioCheckboxLabel: $$IsomorphicComponent;
|
|
38
|
+
type RadioCheckboxLabel<InputType extends HTMLInputTypeAttribute & ('checkbox' | 'radio')> = InstanceType<typeof RadioCheckboxLabel<InputType>>;
|
|
39
|
+
export default RadioCheckboxLabel;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { RadioInputProps, SelectOption } from '../types.js';
|
|
3
3
|
import Fieldset from '../utils/Fieldset.svelte';
|
|
4
|
+
import RadioCheckboxLabel from './RadioCheckboxLabel.svelte';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
7
|
field,
|
|
@@ -30,50 +31,10 @@
|
|
|
30
31
|
}
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
|
-
<Fieldset {label} {labelClass} {className} {disabled}>
|
|
34
|
+
<Fieldset {label} {labelClass} {className} {disabled} type="radio">
|
|
34
35
|
{#each normalizedOptions as option}
|
|
35
36
|
{@const fieldAttrs = getFieldAttrs(option.value)}
|
|
36
37
|
{@const uniqueId = `${name}-${option.value}`}
|
|
37
|
-
<
|
|
38
|
-
<input
|
|
39
|
-
{...fieldAttrs}
|
|
40
|
-
type="radio"
|
|
41
|
-
id={uniqueId}
|
|
42
|
-
disabled={disabled || option.disabled}
|
|
43
|
-
{onblur}
|
|
44
|
-
{oninput}
|
|
45
|
-
/>
|
|
46
|
-
<span class="sform-radio-label">{option.label}</span>
|
|
47
|
-
</label>
|
|
38
|
+
<RadioCheckboxLabel {fieldAttrs} {uniqueId} {option} {disabled} {onblur} {oninput} />
|
|
48
39
|
{/each}
|
|
49
40
|
</Fieldset>
|
|
50
|
-
|
|
51
|
-
<style>
|
|
52
|
-
.sform-radio-group {
|
|
53
|
-
border: none;
|
|
54
|
-
padding: 0;
|
|
55
|
-
margin: 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.sform-radio-group legend {
|
|
59
|
-
padding: 0;
|
|
60
|
-
margin-bottom: 0.5rem;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.sform-radio-option {
|
|
64
|
-
display: flex;
|
|
65
|
-
align-items: center;
|
|
66
|
-
gap: 0.5rem;
|
|
67
|
-
cursor: pointer;
|
|
68
|
-
padding: 0.25rem 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.sform-radio-option.disabled {
|
|
72
|
-
opacity: 0.6;
|
|
73
|
-
cursor: not-allowed;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.sform-radio-option input {
|
|
77
|
-
margin: 0;
|
|
78
|
-
}
|
|
79
|
-
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { RangeInputProps } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -30,9 +31,7 @@
|
|
|
30
31
|
});
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
|
-
{
|
|
34
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
35
|
-
{/if}
|
|
34
|
+
<TopLabel {label} {labelClass} {name} />
|
|
36
35
|
<div class="sform-range-wrapper">
|
|
37
36
|
<input
|
|
38
37
|
{...fieldAttrs}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SelectInputProps, SelectOption } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -24,9 +25,7 @@
|
|
|
24
25
|
);
|
|
25
26
|
</script>
|
|
26
27
|
|
|
27
|
-
{
|
|
28
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
29
|
-
{/if}
|
|
28
|
+
<TopLabel {label} {labelClass} {name} />
|
|
30
29
|
<select {...fieldAttrs} id={name} class={className} {disabled} {onblur} {oninput}>
|
|
31
30
|
{#each normalizedOptions as option (option.value)}
|
|
32
31
|
<option value={option.value} disabled={option.disabled}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TextInputComponentProps } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -28,9 +29,7 @@
|
|
|
28
29
|
let inputElement: HTMLInputElement | undefined = $state();
|
|
29
30
|
</script>
|
|
30
31
|
|
|
31
|
-
{
|
|
32
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
33
|
-
{/if}
|
|
32
|
+
<TopLabel {label} {labelClass} {name} />
|
|
34
33
|
<div class="sform-input-wrapper {wrapperClass ?? ''}">
|
|
35
34
|
{#if prefix}
|
|
36
35
|
<div class="sform-prefix" onclick={() => inputElement?.focus()} role="presentation">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TextareaInputProps } from '../types.js';
|
|
3
|
+
import TopLabel from './TopLabel.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
field,
|
|
@@ -27,9 +28,7 @@
|
|
|
27
28
|
let textareaElement: HTMLTextAreaElement | undefined = $state();
|
|
28
29
|
</script>
|
|
29
30
|
|
|
30
|
-
{
|
|
31
|
-
<label class={labelClass} for={name}>{label}</label>
|
|
32
|
-
{/if}
|
|
31
|
+
<TopLabel {label} {labelClass} {name} />
|
|
33
32
|
<div class="sform-input-wrapper sform-textarea-wrapper {wrapperClass ?? ''}">
|
|
34
33
|
{#if prefix}
|
|
35
34
|
<div class="sform-prefix" onclick={() => textareaElement?.focus()} role="presentation">
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
</script>
|
|
26
26
|
|
|
27
27
|
{#if label}
|
|
28
|
-
|
|
28
|
+
{#if typeof label === 'function'}
|
|
29
|
+
{@render label()}
|
|
30
|
+
{:else}
|
|
31
|
+
<span id="{name}-label" class={labelClass}>{label}</span>
|
|
32
|
+
{/if}
|
|
29
33
|
{/if}
|
|
30
34
|
<label class="sform-toggle {className ?? ''}" class:disabled>
|
|
31
35
|
<input
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BaseInputComponentProps } from '../types';
|
|
3
|
+
|
|
4
|
+
let { label, labelClass, name }: Pick<BaseInputComponentProps, 'label' | 'labelClass' | 'name'> =
|
|
5
|
+
$props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
{#if label}
|
|
9
|
+
{#if typeof label === 'function'}
|
|
10
|
+
{@render label()}
|
|
11
|
+
{:else}
|
|
12
|
+
<label class={labelClass} for={name}>{label}</label>
|
|
13
|
+
{/if}
|
|
14
|
+
{/if}
|
package/dist/Sform/types.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export type NumericInputType = 'number';
|
|
|
96
96
|
*/
|
|
97
97
|
export interface SelectOption {
|
|
98
98
|
value: string;
|
|
99
|
-
label: string
|
|
99
|
+
label: string | Snippet<[SelectOption]>;
|
|
100
100
|
disabled?: boolean;
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
@@ -221,7 +221,7 @@ export interface BaseSfieldProps {
|
|
|
221
221
|
/** Field name - corresponds to form.fields[name] */
|
|
222
222
|
name: string;
|
|
223
223
|
/** Field label */
|
|
224
|
-
label?: string;
|
|
224
|
+
label?: string | Snippet;
|
|
225
225
|
/** Placeholder text */
|
|
226
226
|
placeholder?: string;
|
|
227
227
|
/** Field-level validateOn override */
|
|
@@ -319,9 +319,9 @@ export interface RangeSfieldProps extends BaseSfieldProps {
|
|
|
319
319
|
export interface ToggleSfieldProps extends BaseSfieldProps {
|
|
320
320
|
type: 'toggle';
|
|
321
321
|
/** Label when toggle is on */
|
|
322
|
-
onLabel?: string;
|
|
322
|
+
onLabel?: string | Snippet;
|
|
323
323
|
/** Label when toggle is off */
|
|
324
|
-
offLabel?: string;
|
|
324
|
+
offLabel?: string | Snippet;
|
|
325
325
|
/** Value when checked */
|
|
326
326
|
checkedValue?: string;
|
|
327
327
|
/** Value when unchecked */
|
|
@@ -453,7 +453,7 @@ export interface BaseInputComponentProps {
|
|
|
453
453
|
/** Field name */
|
|
454
454
|
name: string;
|
|
455
455
|
/** Field label */
|
|
456
|
-
label?: string;
|
|
456
|
+
label?: string | Snippet;
|
|
457
457
|
/** Placeholder text */
|
|
458
458
|
placeholder?: string;
|
|
459
459
|
/** Input class */
|
|
@@ -609,7 +609,7 @@ export interface ButtonInputProps<T = unknown> {
|
|
|
609
609
|
/** The remote form - used to infer the result type T */
|
|
610
610
|
form: ButtonFormLike<T>;
|
|
611
611
|
/** Button text (used if no children snippet provided) */
|
|
612
|
-
label?: string;
|
|
612
|
+
label?: string | Snippet;
|
|
613
613
|
/** Button type */
|
|
614
614
|
buttonType?: 'submit' | 'reset' | 'button';
|
|
615
615
|
/** Button class */
|
|
@@ -658,9 +658,9 @@ export interface PasswordInputProps extends BaseInputComponentProps {
|
|
|
658
658
|
*/
|
|
659
659
|
export interface ToggleInputProps extends BaseInputComponentProps {
|
|
660
660
|
/** Label when toggle is on */
|
|
661
|
-
onLabel?: string;
|
|
661
|
+
onLabel?: string | Snippet;
|
|
662
662
|
/** Label when toggle is off */
|
|
663
|
-
offLabel?: string;
|
|
663
|
+
offLabel?: string | Snippet;
|
|
664
664
|
/** Value when checked */
|
|
665
665
|
checkedValue?: string;
|
|
666
666
|
/** Value when unchecked */
|
|
@@ -671,7 +671,7 @@ export interface ToggleInputProps extends BaseInputComponentProps {
|
|
|
671
671
|
*/
|
|
672
672
|
export interface ToggleOption {
|
|
673
673
|
value: string;
|
|
674
|
-
label: string;
|
|
674
|
+
label: string | Snippet;
|
|
675
675
|
disabled?: boolean;
|
|
676
676
|
}
|
|
677
677
|
/**
|
|
@@ -704,9 +704,10 @@ export interface MaskedInputProps extends BaseInputComponentProps, InputAffixPro
|
|
|
704
704
|
* Props for Fieldset utils component
|
|
705
705
|
*/
|
|
706
706
|
export type FieldsetProps = {
|
|
707
|
-
label?: string;
|
|
707
|
+
label?: string | Snippet;
|
|
708
708
|
labelClass?: string;
|
|
709
709
|
className?: string;
|
|
710
710
|
disabled?: boolean;
|
|
711
711
|
children: Snippet;
|
|
712
|
+
type: 'radio' | 'checkbox';
|
|
712
713
|
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { FieldsetProps } from '../types';
|
|
4
4
|
|
|
5
|
-
let { label, labelClass, className, disabled, children }: FieldsetProps = $props();
|
|
5
|
+
let { label, labelClass, className, disabled, children, type }: FieldsetProps = $props();
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
|
-
<fieldset class="sform-
|
|
8
|
+
<fieldset class="{`sform-${type}-group`} {className ?? ''}" {disabled}>
|
|
9
9
|
{#if label}
|
|
10
10
|
<legend class={labelClass}>{label}</legend>
|
|
11
11
|
{/if}
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
</fieldset>
|
|
14
14
|
|
|
15
15
|
<style>
|
|
16
|
-
.sform-checkbox-group
|
|
16
|
+
:is(.sform-checkbox-group, .sform-radio-group) {
|
|
17
|
+
border: none;
|
|
18
|
+
padding: 0;
|
|
19
|
+
margin: 0;
|
|
20
|
+
}
|
|
21
|
+
:is(.sform-checkbox-group, .sform-radio-group) legend {
|
|
17
22
|
padding: 0.25rem 0.5rem;
|
|
18
23
|
margin: 0;
|
|
19
24
|
border: var(--sform-border-width) solid var(--sform-border);
|
package/package.json
CHANGED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { ToggleOptionsInputProps, ToggleOption } from '../types.js';
|
|
3
|
-
|
|
4
|
-
let {
|
|
5
|
-
field,
|
|
6
|
-
name,
|
|
7
|
-
label,
|
|
8
|
-
class: className,
|
|
9
|
-
labelClass,
|
|
10
|
-
disabled,
|
|
11
|
-
options,
|
|
12
|
-
multiple = false,
|
|
13
|
-
showIssues,
|
|
14
|
-
onblur,
|
|
15
|
-
oninput
|
|
16
|
-
}: ToggleOptionsInputProps = $props();
|
|
17
|
-
|
|
18
|
-
// Normalize options to ToggleOption format
|
|
19
|
-
const normalizedOptions: ToggleOption[] = $derived(
|
|
20
|
-
options.map((opt) => (typeof opt === 'string' ? { value: opt, label: opt } : opt))
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
// Helper to get field attrs with controlled aria-invalid
|
|
24
|
-
function getFieldAttrs(optionValue: string) {
|
|
25
|
-
const inputType = multiple ? 'checkbox' : 'radio';
|
|
26
|
-
const attrs = field.as(inputType, optionValue);
|
|
27
|
-
return {
|
|
28
|
-
...attrs,
|
|
29
|
-
'aria-invalid': showIssues ? attrs['aria-invalid'] : undefined
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
</script>
|
|
33
|
-
|
|
34
|
-
{#if label}
|
|
35
|
-
<span id="{name}-label" class={labelClass}>{label}</span>
|
|
36
|
-
{/if}
|
|
37
|
-
<div
|
|
38
|
-
class="sform-toggle-options {className ?? ''}"
|
|
39
|
-
role={multiple ? 'group' : 'radiogroup'}
|
|
40
|
-
aria-labelledby={label ? `${name}-label` : undefined}
|
|
41
|
-
aria-label={!label ? name : undefined}
|
|
42
|
-
>
|
|
43
|
-
{#each normalizedOptions as option (option.value)}
|
|
44
|
-
{@const optionDisabled = disabled || option.disabled}
|
|
45
|
-
{@const fieldAttrs = getFieldAttrs(option.value)}
|
|
46
|
-
{@const inputId = `${name}-${option.value}`}
|
|
47
|
-
<label class="sform-toggle-option" class:disabled={optionDisabled} for={inputId}>
|
|
48
|
-
<input
|
|
49
|
-
{...fieldAttrs}
|
|
50
|
-
type={multiple ? 'checkbox' : 'radio'}
|
|
51
|
-
id={inputId}
|
|
52
|
-
disabled={optionDisabled}
|
|
53
|
-
{onblur}
|
|
54
|
-
{oninput}
|
|
55
|
-
/>
|
|
56
|
-
<span>{option.label}</span>
|
|
57
|
-
</label>
|
|
58
|
-
{/each}
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<style>
|
|
62
|
-
.sform-toggle-options {
|
|
63
|
-
display: inline-flex;
|
|
64
|
-
border-radius: 0.375rem;
|
|
65
|
-
overflow: hidden;
|
|
66
|
-
border: 1px solid #ccc;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.sform-toggle-option {
|
|
70
|
-
display: flex;
|
|
71
|
-
align-items: center;
|
|
72
|
-
justify-content: center;
|
|
73
|
-
padding: 0.5rem 1rem;
|
|
74
|
-
background: white;
|
|
75
|
-
cursor: pointer;
|
|
76
|
-
transition:
|
|
77
|
-
background-color 0.15s,
|
|
78
|
-
color 0.15s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.sform-toggle-option:not(:last-child) {
|
|
82
|
-
border-right: 1px solid #ccc;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.sform-toggle-option:hover:not(.disabled) {
|
|
86
|
-
background-color: #f5f5f5;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* Hide the actual input but keep it functional */
|
|
90
|
-
.sform-toggle-option input {
|
|
91
|
-
position: absolute;
|
|
92
|
-
opacity: 0;
|
|
93
|
-
width: 0;
|
|
94
|
-
height: 0;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* Style when checked */
|
|
98
|
-
.sform-toggle-option:has(input:checked) {
|
|
99
|
-
background-color: #2196f3;
|
|
100
|
-
color: white;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.sform-toggle-option:has(input:checked):hover {
|
|
104
|
-
background-color: #1976d2;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.sform-toggle-option.disabled {
|
|
108
|
-
opacity: 0.5;
|
|
109
|
-
cursor: not-allowed;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* Focus styles */
|
|
113
|
-
.sform-toggle-option:has(input:focus-visible) {
|
|
114
|
-
outline: 2px solid #2196f3;
|
|
115
|
-
outline-offset: -2px;
|
|
116
|
-
z-index: 1;
|
|
117
|
-
}
|
|
118
|
-
</style>
|