@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 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
@@ -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'}
@@ -53,7 +53,6 @@
53
53
 
54
54
  // Trigger validation including untouched fields (for blur mode)
55
55
  const triggerValidation = () => {
56
- if (form.pending) return;
57
56
  form.validate({ includeUntouched: true, preflightOnly });
58
57
  };
59
58
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" generics="T = unknown">
2
- import type { Snippet } from 'svelte';
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 children}
84
- {@render children(formState)}
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 { Snippet } from 'svelte';
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
- <label class="sform-checkbox-option" class:disabled={option.disabled}>
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
- <span class={labelClass}>{label}</span>
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
- {#if label}
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
- {#if label}
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
- {#if label}
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
- <label class="sform-radio-option" class:disabled={option.disabled}>
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
- {#if label}
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
- {#if label}
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
- {#if label}
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
- {#if label}
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
- <span id="{name}-label" class={labelClass}>{label}</span>
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}
@@ -0,0 +1,4 @@
1
+ import type { BaseInputComponentProps } from '../types';
2
+ declare const TopLabel: import("svelte").Component<Pick<BaseInputComponentProps, "name" | "labelClass" | "label">, {}, "">;
3
+ type TopLabel = ReturnType<typeof TopLabel>;
4
+ export default TopLabel;
@@ -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-checkbox-group {className ?? ''}" {disabled}>
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 legend {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@samuel-charpentier/sform",
3
- "version": "0.0.11",
3
+ "version": "1.0.0",
4
4
  "description": "A Svelte 5 form library for SvelteKit remote forms",
5
5
  "keywords": [
6
6
  "svelte",
@@ -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>
@@ -1,4 +0,0 @@
1
- import type { ToggleOptionsInputProps } from '../types.js';
2
- declare const ToggleOptionsInput: import("svelte").Component<ToggleOptionsInputProps, {}, "">;
3
- type ToggleOptionsInput = ReturnType<typeof ToggleOptionsInput>;
4
- export default ToggleOptionsInput;