@privaty/ui-forms 0.1.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +107 -0
- package/dist/components/form-error.svelte +49 -0
- package/dist/components/form-error.svelte.d.ts +15 -0
- package/dist/components/reset.svelte +49 -0
- package/dist/components/reset.svelte.d.ts +19 -0
- package/dist/components/submit.svelte +82 -0
- package/dist/components/submit.svelte.d.ts +27 -0
- package/dist/context.d.ts +23 -0
- package/dist/context.js +21 -0
- package/dist/form-state.svelte.d.ts +89 -0
- package/dist/form-state.svelte.js +139 -0
- package/dist/form.svelte +219 -0
- package/dist/form.svelte.d.ts +62 -0
- package/dist/inputs/checkbox-input.svelte +119 -0
- package/dist/inputs/checkbox-input.svelte.d.ts +45 -0
- package/dist/inputs/date-input.svelte +132 -0
- package/dist/inputs/date-input.svelte.d.ts +61 -0
- package/dist/inputs/number-input.svelte +141 -0
- package/dist/inputs/number-input.svelte.d.ts +63 -0
- package/dist/inputs/select-input.svelte +156 -0
- package/dist/inputs/select-input.svelte.d.ts +57 -0
- package/dist/inputs/text-input.svelte +126 -0
- package/dist/inputs/text-input.svelte.d.ts +59 -0
- package/dist/inputs/textarea-input.svelte +135 -0
- package/dist/inputs/textarea-input.svelte.d.ts +57 -0
- package/dist/inputs/wire-field.d.ts +44 -0
- package/dist/inputs/wire-field.js +45 -0
- package/dist/testing/fakes.svelte.d.ts +205 -0
- package/dist/testing/fakes.svelte.js +346 -0
- package/dist/types/field.d.ts +156 -0
- package/dist/types/field.js +1 -0
- package/dist/types/form.d.ts +27 -0
- package/dist/types/form.js +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Date-family input (date, datetime-local, month, time, week) wired to a
|
|
3
|
+
SvelteKit remote form field — must render inside a <Form>, whose context it
|
|
4
|
+
registers with. Values are ISO-style strings in the chosen type's format.
|
|
5
|
+
Shows resolver-translated validation errors once the form state reveals them,
|
|
6
|
+
plus a majority-aware required/optional marker, and turns readonly while the
|
|
7
|
+
form submits.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { cn } from "@privaty/ui/cn.js";
|
|
11
|
+
import Input from "@privaty/ui/components/input.svelte";
|
|
12
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
13
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
14
|
+
import type { DateField, DateFieldType } from "../types/field";
|
|
15
|
+
import { wireField } from "./wire-field";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
19
|
+
* satisfying the DateField slice works, e.g. a fake from the public
|
|
20
|
+
* testing subpath. */
|
|
21
|
+
field: DateField;
|
|
22
|
+
/** Visible label text for the control. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Which date-family input to render: "date" (default),
|
|
25
|
+
* "datetime-local", "month", "time", or "week". */
|
|
26
|
+
type?: DateFieldType;
|
|
27
|
+
|
|
28
|
+
/** Label placement: "top" (default), "left", or "hidden". No floating:
|
|
29
|
+
* date-family inputs render their format scaffold even when empty, so
|
|
30
|
+
* :placeholder-shown never behaves. */
|
|
31
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
32
|
+
|
|
33
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
34
|
+
* marker (required markers styled red) — forms where at least half the
|
|
35
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
36
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
37
|
+
* as a native `required` attribute. */
|
|
38
|
+
required?: boolean;
|
|
39
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
40
|
+
* never disable to lock the form during submit; the input already turns
|
|
41
|
+
* readonly while submitting. */
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
44
|
+
* submitting, independent of this prop. */
|
|
45
|
+
readonly?: boolean;
|
|
46
|
+
|
|
47
|
+
/** ISO-style string in the chosen type's format (e.g. "2026-08" for
|
|
48
|
+
* month, "2026-08-18" for date). */
|
|
49
|
+
initialValue?: string;
|
|
50
|
+
|
|
51
|
+
/** Native lower bound, in the same format as the value. */
|
|
52
|
+
min?: string;
|
|
53
|
+
/** Native upper bound, in the same format as the value. */
|
|
54
|
+
max?: string;
|
|
55
|
+
|
|
56
|
+
/** Passed through to the native autocomplete attribute (e.g. "bday"). */
|
|
57
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
58
|
+
|
|
59
|
+
/** Extra classes for the outer field wrapper. */
|
|
60
|
+
class?: string;
|
|
61
|
+
/** Extra classes for the <label> element. */
|
|
62
|
+
labelClass?: string;
|
|
63
|
+
/** Extra classes for the <input> element. */
|
|
64
|
+
inputClass?: string;
|
|
65
|
+
/** Extra classes for the required/optional marker. */
|
|
66
|
+
markerClass?: string;
|
|
67
|
+
/** Extra classes for the error <ul>. */
|
|
68
|
+
errorClass?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
field,
|
|
73
|
+
label,
|
|
74
|
+
type = "date",
|
|
75
|
+
|
|
76
|
+
labelStyle,
|
|
77
|
+
|
|
78
|
+
required = false,
|
|
79
|
+
disabled = false,
|
|
80
|
+
readonly = false,
|
|
81
|
+
|
|
82
|
+
initialValue = "",
|
|
83
|
+
|
|
84
|
+
min,
|
|
85
|
+
max,
|
|
86
|
+
|
|
87
|
+
autocomplete,
|
|
88
|
+
|
|
89
|
+
class: classes,
|
|
90
|
+
labelClass,
|
|
91
|
+
inputClass,
|
|
92
|
+
markerClass,
|
|
93
|
+
errorClass,
|
|
94
|
+
}: Props = $props();
|
|
95
|
+
|
|
96
|
+
const attributes = $derived(field.as(type, initialValue));
|
|
97
|
+
|
|
98
|
+
// The field, type, and initialValue are stable for the component's lifetime,
|
|
99
|
+
// so capturing the initial name and registration is intentional.
|
|
100
|
+
// svelte-ignore state_referenced_locally
|
|
101
|
+
const name = attributes.name;
|
|
102
|
+
|
|
103
|
+
// svelte-ignore state_referenced_locally
|
|
104
|
+
const wired = wireField({
|
|
105
|
+
name,
|
|
106
|
+
initialValue,
|
|
107
|
+
required,
|
|
108
|
+
issues: () => field.issues(),
|
|
109
|
+
getValue: () => field.value(),
|
|
110
|
+
setValue: (value) => field.set(value as string),
|
|
111
|
+
normalize: (value) => (value == null ? "" : String(value)),
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<Input
|
|
116
|
+
{...attributes}
|
|
117
|
+
{label}
|
|
118
|
+
{labelStyle}
|
|
119
|
+
errors={wired.errors}
|
|
120
|
+
marker={wired.marker}
|
|
121
|
+
aria-invalid={wired.errors.length > 0 ? true : undefined}
|
|
122
|
+
{disabled}
|
|
123
|
+
readonly={readonly || wired.state.isSubmitting}
|
|
124
|
+
{min}
|
|
125
|
+
{max}
|
|
126
|
+
{autocomplete}
|
|
127
|
+
class={classes}
|
|
128
|
+
{labelClass}
|
|
129
|
+
{inputClass}
|
|
130
|
+
markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
|
|
131
|
+
{errorClass}
|
|
132
|
+
/>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
3
|
+
import type { DateField, DateFieldType } from "../types/field";
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
6
|
+
* satisfying the DateField slice works, e.g. a fake from the public
|
|
7
|
+
* testing subpath. */
|
|
8
|
+
field: DateField;
|
|
9
|
+
/** Visible label text for the control. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Which date-family input to render: "date" (default),
|
|
12
|
+
* "datetime-local", "month", "time", or "week". */
|
|
13
|
+
type?: DateFieldType;
|
|
14
|
+
/** Label placement: "top" (default), "left", or "hidden". No floating:
|
|
15
|
+
* date-family inputs render their format scaffold even when empty, so
|
|
16
|
+
* :placeholder-shown never behaves. */
|
|
17
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
18
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
19
|
+
* marker (required markers styled red) — forms where at least half the
|
|
20
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
21
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
22
|
+
* as a native `required` attribute. */
|
|
23
|
+
required?: boolean;
|
|
24
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
25
|
+
* never disable to lock the form during submit; the input already turns
|
|
26
|
+
* readonly while submitting. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
29
|
+
* submitting, independent of this prop. */
|
|
30
|
+
readonly?: boolean;
|
|
31
|
+
/** ISO-style string in the chosen type's format (e.g. "2026-08" for
|
|
32
|
+
* month, "2026-08-18" for date). */
|
|
33
|
+
initialValue?: string;
|
|
34
|
+
/** Native lower bound, in the same format as the value. */
|
|
35
|
+
min?: string;
|
|
36
|
+
/** Native upper bound, in the same format as the value. */
|
|
37
|
+
max?: string;
|
|
38
|
+
/** Passed through to the native autocomplete attribute (e.g. "bday"). */
|
|
39
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
40
|
+
/** Extra classes for the outer field wrapper. */
|
|
41
|
+
class?: string;
|
|
42
|
+
/** Extra classes for the <label> element. */
|
|
43
|
+
labelClass?: string;
|
|
44
|
+
/** Extra classes for the <input> element. */
|
|
45
|
+
inputClass?: string;
|
|
46
|
+
/** Extra classes for the required/optional marker. */
|
|
47
|
+
markerClass?: string;
|
|
48
|
+
/** Extra classes for the error <ul>. */
|
|
49
|
+
errorClass?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Date-family input (date, datetime-local, month, time, week) wired to a
|
|
53
|
+
* SvelteKit remote form field — must render inside a <Form>, whose context it
|
|
54
|
+
* registers with. Values are ISO-style strings in the chosen type's format.
|
|
55
|
+
* Shows resolver-translated validation errors once the form state reveals them,
|
|
56
|
+
* plus a majority-aware required/optional marker, and turns readonly while the
|
|
57
|
+
* form submits.
|
|
58
|
+
*/
|
|
59
|
+
declare const DateInput: import("svelte").Component<Props, {}, "">;
|
|
60
|
+
type DateInput = ReturnType<typeof DateInput>;
|
|
61
|
+
export default DateInput;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Number input wired to a SvelteKit remote form number field — must render
|
|
3
|
+
inside a <Form>, whose context it registers with. Shows resolver-translated
|
|
4
|
+
validation errors once the form state reveals them, plus a majority-aware
|
|
5
|
+
required/optional marker, and turns readonly while the form submits.
|
|
6
|
+
Mid-edit the field holds the raw DOM string — Kit only coerces to number at
|
|
7
|
+
submit/reset — and a cleared input counts as unset.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { cn } from "@privaty/ui/cn.js";
|
|
11
|
+
import Input from "@privaty/ui/components/input.svelte";
|
|
12
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
13
|
+
import type { NumberField } from "../types/field";
|
|
14
|
+
import { wireField } from "./wire-field";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
18
|
+
* satisfying the NumberField slice works, e.g. a fake from the public
|
|
19
|
+
* testing subpath. */
|
|
20
|
+
field: NumberField;
|
|
21
|
+
/** Visible label text. In the floating label style it doubles as the
|
|
22
|
+
* placeholder. */
|
|
23
|
+
label: string;
|
|
24
|
+
|
|
25
|
+
/** Label placement: "top" (default), "left", "floating", or "hidden"
|
|
26
|
+
* (visually hidden, still read by screen readers). */
|
|
27
|
+
labelStyle?: LabelStyle;
|
|
28
|
+
|
|
29
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
30
|
+
* marker (required markers styled red) — forms where at least half the
|
|
31
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
32
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
33
|
+
* as a native `required` attribute. */
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
36
|
+
* never disable to lock the form during submit; the input already turns
|
|
37
|
+
* readonly while submitting. */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
40
|
+
* submitting, independent of this prop. */
|
|
41
|
+
readonly?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Seed handed to Kit's `as()` — the value native form reset restores
|
|
44
|
+
* and the baseline for dirty tracking (a cleared input compares as
|
|
45
|
+
* unset). Omitted means unseeded. Assumed stable for the component's
|
|
46
|
+
* lifetime. */
|
|
47
|
+
initialValue?: number;
|
|
48
|
+
|
|
49
|
+
/** Native `min` constraint, forwarded to the input. */
|
|
50
|
+
min?: number;
|
|
51
|
+
/** Native `max` constraint, forwarded to the input. */
|
|
52
|
+
max?: number;
|
|
53
|
+
/** Native `step` granularity, forwarded to the input — "any" permits any
|
|
54
|
+
* decimal. */
|
|
55
|
+
step?: number | "any";
|
|
56
|
+
|
|
57
|
+
/** Placeholder text — ignored in the floating label style, where the
|
|
58
|
+
* label plays that role. */
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
|
|
61
|
+
/** Extra classes for the outer field wrapper. */
|
|
62
|
+
class?: string;
|
|
63
|
+
/** Extra classes for the <label> element. */
|
|
64
|
+
labelClass?: string;
|
|
65
|
+
/** Extra classes for the <input> element. */
|
|
66
|
+
inputClass?: string;
|
|
67
|
+
/** Extra classes for the required/optional marker. */
|
|
68
|
+
markerClass?: string;
|
|
69
|
+
/** Extra classes for the error <ul>. */
|
|
70
|
+
errorClass?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
field,
|
|
75
|
+
label,
|
|
76
|
+
|
|
77
|
+
labelStyle,
|
|
78
|
+
|
|
79
|
+
required = false,
|
|
80
|
+
disabled = false,
|
|
81
|
+
readonly = false,
|
|
82
|
+
|
|
83
|
+
initialValue,
|
|
84
|
+
|
|
85
|
+
min,
|
|
86
|
+
max,
|
|
87
|
+
step,
|
|
88
|
+
|
|
89
|
+
placeholder,
|
|
90
|
+
|
|
91
|
+
class: classes,
|
|
92
|
+
labelClass,
|
|
93
|
+
inputClass,
|
|
94
|
+
markerClass,
|
|
95
|
+
errorClass,
|
|
96
|
+
}: Props = $props();
|
|
97
|
+
|
|
98
|
+
const attributes = $derived(
|
|
99
|
+
initialValue === undefined
|
|
100
|
+
? field.as("number")
|
|
101
|
+
: field.as("number", initialValue),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// The field and initialValue are stable for the component's lifetime, so
|
|
105
|
+
// capturing the initial name and registration is intentional.
|
|
106
|
+
// svelte-ignore state_referenced_locally
|
|
107
|
+
const name = attributes.name;
|
|
108
|
+
|
|
109
|
+
// svelte-ignore state_referenced_locally
|
|
110
|
+
const wired = wireField({
|
|
111
|
+
name,
|
|
112
|
+
initialValue,
|
|
113
|
+
required,
|
|
114
|
+
issues: () => field.issues(),
|
|
115
|
+
getValue: () => field.value(),
|
|
116
|
+
setValue: (value) => field.set(value as number),
|
|
117
|
+
// Kit stores the raw DOM string mid-edit; "" (cleared) means unset.
|
|
118
|
+
normalize: (value) =>
|
|
119
|
+
value === "" || value == null ? undefined : Number(value),
|
|
120
|
+
});
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<Input
|
|
124
|
+
{...attributes}
|
|
125
|
+
{label}
|
|
126
|
+
{labelStyle}
|
|
127
|
+
errors={wired.errors}
|
|
128
|
+
marker={wired.marker}
|
|
129
|
+
aria-invalid={wired.errors.length > 0 ? true : undefined}
|
|
130
|
+
{disabled}
|
|
131
|
+
readonly={readonly || wired.state.isSubmitting}
|
|
132
|
+
{min}
|
|
133
|
+
{max}
|
|
134
|
+
{step}
|
|
135
|
+
{placeholder}
|
|
136
|
+
class={classes}
|
|
137
|
+
{labelClass}
|
|
138
|
+
{inputClass}
|
|
139
|
+
markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
|
|
140
|
+
{errorClass}
|
|
141
|
+
/>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LabelStyle } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { NumberField } from "../types/field";
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
5
|
+
* satisfying the NumberField slice works, e.g. a fake from the public
|
|
6
|
+
* testing subpath. */
|
|
7
|
+
field: NumberField;
|
|
8
|
+
/** Visible label text. In the floating label style it doubles as the
|
|
9
|
+
* placeholder. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Label placement: "top" (default), "left", "floating", or "hidden"
|
|
12
|
+
* (visually hidden, still read by screen readers). */
|
|
13
|
+
labelStyle?: LabelStyle;
|
|
14
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
15
|
+
* marker (required markers styled red) — forms where at least half the
|
|
16
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
17
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
18
|
+
* as a native `required` attribute. */
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/** Disables the control. Disabled controls are excluded from FormData —
|
|
21
|
+
* never disable to lock the form during submit; the input already turns
|
|
22
|
+
* readonly while submitting. */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Renders the input readonly. Also forced on while the form is
|
|
25
|
+
* submitting, independent of this prop. */
|
|
26
|
+
readonly?: boolean;
|
|
27
|
+
/** Seed handed to Kit's `as()` — the value native form reset restores
|
|
28
|
+
* and the baseline for dirty tracking (a cleared input compares as
|
|
29
|
+
* unset). Omitted means unseeded. Assumed stable for the component's
|
|
30
|
+
* lifetime. */
|
|
31
|
+
initialValue?: number;
|
|
32
|
+
/** Native `min` constraint, forwarded to the input. */
|
|
33
|
+
min?: number;
|
|
34
|
+
/** Native `max` constraint, forwarded to the input. */
|
|
35
|
+
max?: number;
|
|
36
|
+
/** Native `step` granularity, forwarded to the input — "any" permits any
|
|
37
|
+
* decimal. */
|
|
38
|
+
step?: number | "any";
|
|
39
|
+
/** Placeholder text — ignored in the floating label style, where the
|
|
40
|
+
* label plays that role. */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/** Extra classes for the outer field wrapper. */
|
|
43
|
+
class?: string;
|
|
44
|
+
/** Extra classes for the <label> element. */
|
|
45
|
+
labelClass?: string;
|
|
46
|
+
/** Extra classes for the <input> element. */
|
|
47
|
+
inputClass?: string;
|
|
48
|
+
/** Extra classes for the required/optional marker. */
|
|
49
|
+
markerClass?: string;
|
|
50
|
+
/** Extra classes for the error <ul>. */
|
|
51
|
+
errorClass?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Number input wired to a SvelteKit remote form number field — must render
|
|
55
|
+
* inside a <Form>, whose context it registers with. Shows resolver-translated
|
|
56
|
+
* validation errors once the form state reveals them, plus a majority-aware
|
|
57
|
+
* required/optional marker, and turns readonly while the form submits.
|
|
58
|
+
* Mid-edit the field holds the raw DOM string — Kit only coerces to number at
|
|
59
|
+
* submit/reset — and a cleared input counts as unset.
|
|
60
|
+
*/
|
|
61
|
+
declare const NumberInput: import("svelte").Component<Props, {}, "">;
|
|
62
|
+
type NumberInput = ReturnType<typeof NumberInput>;
|
|
63
|
+
export default NumberInput;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Select wired to a SvelteKit remote form field — must render inside a <Form>,
|
|
3
|
+
whose context it registers with. Shows resolver-translated validation errors
|
|
4
|
+
once the form state reveals them, plus a majority-aware required/optional
|
|
5
|
+
marker. While submitting it locks interaction with CSS and key swallowing
|
|
6
|
+
instead of disabling — a disabled select is excluded from FormData.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { cn } from "@privaty/ui/cn.js";
|
|
10
|
+
import Select from "@privaty/ui/components/select.svelte";
|
|
11
|
+
import type {
|
|
12
|
+
LabelStyle,
|
|
13
|
+
SelectOption,
|
|
14
|
+
} from "@privaty/ui/components/types.js";
|
|
15
|
+
import type { SelectField } from "../types/field";
|
|
16
|
+
import { wireField } from "./wire-field";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
20
|
+
* satisfying the SelectField slice works, e.g. a fake from the public
|
|
21
|
+
* testing subpath. */
|
|
22
|
+
field: SelectField;
|
|
23
|
+
/** Visible label text for the control. */
|
|
24
|
+
label: string;
|
|
25
|
+
|
|
26
|
+
/** Options to render, in order. A plain string is shorthand for
|
|
27
|
+
* `{ value: s, label: s }`; values must be unique — they key the
|
|
28
|
+
* rendered list. */
|
|
29
|
+
options: readonly (string | SelectOption)[];
|
|
30
|
+
/** Rendered as a disabled empty option, shown until a value is chosen.
|
|
31
|
+
* Its presence also makes "" the default seed for an unseeded field. */
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
|
|
34
|
+
/** Label placement: "top" (default), "left", or "hidden" — floating is
|
|
35
|
+
* not offered for selects. */
|
|
36
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
37
|
+
|
|
38
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
39
|
+
* marker (required markers styled red) — forms where at least half the
|
|
40
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
41
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
42
|
+
* as a native `required` attribute. */
|
|
43
|
+
required?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Selects have no native readonly, so submitting locks interaction with
|
|
46
|
+
* CSS instead. NEVER disable while submitting: disabled controls are
|
|
47
|
+
* excluded from FormData, and Kit validates live form data mid-submission
|
|
48
|
+
* — a disabled select vanishes from it and fails its own validation.
|
|
49
|
+
*/
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Seeds the field and marks the matching option `selected`, so native
|
|
53
|
+
* reset restores it. Omitted with a placeholder present, the field is
|
|
54
|
+
* seeded with "" and the placeholder is the initial state. */
|
|
55
|
+
initialValue?: string;
|
|
56
|
+
|
|
57
|
+
/** Extra classes for the outer field wrapper. */
|
|
58
|
+
class?: string;
|
|
59
|
+
/** Extra classes for the <label> element. */
|
|
60
|
+
labelClass?: string;
|
|
61
|
+
/** Extra classes for the <select> element. */
|
|
62
|
+
selectClass?: string;
|
|
63
|
+
/** Extra classes for the required/optional marker. */
|
|
64
|
+
markerClass?: string;
|
|
65
|
+
/** Extra classes for the error <ul>. */
|
|
66
|
+
errorClass?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
field,
|
|
71
|
+
label,
|
|
72
|
+
|
|
73
|
+
options,
|
|
74
|
+
placeholder,
|
|
75
|
+
|
|
76
|
+
labelStyle,
|
|
77
|
+
|
|
78
|
+
required = false,
|
|
79
|
+
disabled = false,
|
|
80
|
+
|
|
81
|
+
initialValue,
|
|
82
|
+
|
|
83
|
+
class: classes,
|
|
84
|
+
labelClass,
|
|
85
|
+
selectClass,
|
|
86
|
+
markerClass,
|
|
87
|
+
errorClass,
|
|
88
|
+
}: Props = $props();
|
|
89
|
+
|
|
90
|
+
// With a placeholder, the natural empty state is the placeholder option —
|
|
91
|
+
// so an unseeded field is seeded with "". Without the seed, browsers select
|
|
92
|
+
// the first enabled option (skipping the disabled placeholder) while the
|
|
93
|
+
// field state stays unset, so the submission would carry no value at all.
|
|
94
|
+
// svelte-ignore state_referenced_locally
|
|
95
|
+
const seed = initialValue ?? (placeholder !== undefined ? "" : undefined);
|
|
96
|
+
|
|
97
|
+
const attributes = $derived(
|
|
98
|
+
seed === undefined ? field.as("select") : field.as("select", seed),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// The field and seed are stable for the component's lifetime, so capturing
|
|
102
|
+
// the initial name and registration is intentional.
|
|
103
|
+
// svelte-ignore state_referenced_locally
|
|
104
|
+
const name = attributes.name;
|
|
105
|
+
|
|
106
|
+
// svelte-ignore state_referenced_locally
|
|
107
|
+
const wired = wireField({
|
|
108
|
+
name,
|
|
109
|
+
initialValue: seed,
|
|
110
|
+
required,
|
|
111
|
+
issues: () => field.issues(),
|
|
112
|
+
getValue: () => field.value(),
|
|
113
|
+
setValue: (value) => field.set(value as never),
|
|
114
|
+
normalize: (value) => (value == null ? "" : String(value)),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// pointer-events-none blocks the mouse while submitting but not the
|
|
118
|
+
// keyboard — swallow value-changing keys too (Tab stays free).
|
|
119
|
+
function lockKeysWhileSubmitting(event: KeyboardEvent) {
|
|
120
|
+
if (!wired.state.isSubmitting) return;
|
|
121
|
+
if (
|
|
122
|
+
[
|
|
123
|
+
"ArrowUp",
|
|
124
|
+
"ArrowDown",
|
|
125
|
+
"ArrowLeft",
|
|
126
|
+
"ArrowRight",
|
|
127
|
+
" ",
|
|
128
|
+
"Enter",
|
|
129
|
+
].includes(event.key)
|
|
130
|
+
) {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<Select
|
|
137
|
+
{...attributes}
|
|
138
|
+
{label}
|
|
139
|
+
{labelStyle}
|
|
140
|
+
{options}
|
|
141
|
+
{placeholder}
|
|
142
|
+
errors={wired.errors}
|
|
143
|
+
marker={wired.marker}
|
|
144
|
+
aria-invalid={wired.errors.length > 0 ? true : undefined}
|
|
145
|
+
defaultValue={seed}
|
|
146
|
+
onkeydown={lockKeysWhileSubmitting}
|
|
147
|
+
{disabled}
|
|
148
|
+
class={classes}
|
|
149
|
+
{labelClass}
|
|
150
|
+
selectClass={cn(
|
|
151
|
+
wired.state.isSubmitting && "pointer-events-none",
|
|
152
|
+
selectClass,
|
|
153
|
+
)}
|
|
154
|
+
markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
|
|
155
|
+
{errorClass}
|
|
156
|
+
/>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { LabelStyle, SelectOption } from "@privaty/ui/components/types.js";
|
|
2
|
+
import type { SelectField } from "../types/field";
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The SvelteKit remote form field to bind. Structural — anything
|
|
5
|
+
* satisfying the SelectField slice works, e.g. a fake from the public
|
|
6
|
+
* testing subpath. */
|
|
7
|
+
field: SelectField;
|
|
8
|
+
/** Visible label text for the control. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Options to render, in order. A plain string is shorthand for
|
|
11
|
+
* `{ value: s, label: s }`; values must be unique — they key the
|
|
12
|
+
* rendered list. */
|
|
13
|
+
options: readonly (string | SelectOption)[];
|
|
14
|
+
/** Rendered as a disabled empty option, shown until a value is chosen.
|
|
15
|
+
* Its presence also makes "" the default seed for an unseeded field. */
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
/** Label placement: "top" (default), "left", or "hidden" — floating is
|
|
18
|
+
* not offered for selects. */
|
|
19
|
+
labelStyle?: Exclude<LabelStyle, "floating">;
|
|
20
|
+
/** Marks the field required: feeds the majority-aware required/optional
|
|
21
|
+
* marker (required markers styled red) — forms where at least half the
|
|
22
|
+
* fields are required mark the optional ones instead. Validation itself
|
|
23
|
+
* comes from the field's schema, not this flag, and it is not forwarded
|
|
24
|
+
* as a native `required` attribute. */
|
|
25
|
+
required?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Selects have no native readonly, so submitting locks interaction with
|
|
28
|
+
* CSS instead. NEVER disable while submitting: disabled controls are
|
|
29
|
+
* excluded from FormData, and Kit validates live form data mid-submission
|
|
30
|
+
* — a disabled select vanishes from it and fails its own validation.
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Seeds the field and marks the matching option `selected`, so native
|
|
34
|
+
* reset restores it. Omitted with a placeholder present, the field is
|
|
35
|
+
* seeded with "" and the placeholder is the initial state. */
|
|
36
|
+
initialValue?: string;
|
|
37
|
+
/** Extra classes for the outer field wrapper. */
|
|
38
|
+
class?: string;
|
|
39
|
+
/** Extra classes for the <label> element. */
|
|
40
|
+
labelClass?: string;
|
|
41
|
+
/** Extra classes for the <select> element. */
|
|
42
|
+
selectClass?: string;
|
|
43
|
+
/** Extra classes for the required/optional marker. */
|
|
44
|
+
markerClass?: string;
|
|
45
|
+
/** Extra classes for the error <ul>. */
|
|
46
|
+
errorClass?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Select wired to a SvelteKit remote form field — must render inside a <Form>,
|
|
50
|
+
* whose context it registers with. Shows resolver-translated validation errors
|
|
51
|
+
* once the form state reveals them, plus a majority-aware required/optional
|
|
52
|
+
* marker. While submitting it locks interaction with CSS and key swallowing
|
|
53
|
+
* instead of disabling — a disabled select is excluded from FormData.
|
|
54
|
+
*/
|
|
55
|
+
declare const SelectInput: import("svelte").Component<Props, {}, "">;
|
|
56
|
+
type SelectInput = ReturnType<typeof SelectInput>;
|
|
57
|
+
export default SelectInput;
|