@privaty/ui-forms 0.1.5 → 0.2.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
@@ -43,6 +43,33 @@ component, all string-valued), `SelectInput`, `CheckboxInput`. Plus
43
43
  `Submit`, `Reset`, `FormError`. Icon-style Submit/Reset: pass children; the
44
44
  label stays as the accessible name.
45
45
 
46
+ ## Picker inputs
47
+
48
+ `DatePickerInput`, `MonthPickerInput`, and `WeekPickerInput` pair the
49
+ native input with the core library's cross-browser calendar pickers — the
50
+ custom month/week UIs Firefox never got, and one consistent picker
51
+ everywhere else.
52
+
53
+ - The **native input stays visible as the FormData carrier**: typing works,
54
+ native mobile pickers stay, and Kit reads live form data as ever. Firefox
55
+ renders month/week carriers as plain text inputs — exactly the gap the
56
+ calendar button fills.
57
+ - **Firefox date exception**: Firefox draws an unhideable calendar icon on
58
+ `type="date"` (Bugzilla 1830890), so `DatePickerInput` yields there — the
59
+ native icon opens Firefox's own picker and our trigger hides, keeping one
60
+ icon in every browser. Month/week use our picker everywhere.
61
+ - The overlaid calendar button opens the picker in an anchored `Popover`
62
+ (top layer, light dismiss, zero-lag scroll tracking). A pick **writes the
63
+ input the way typing does** (DOM value + a bubbling `input` event), so
64
+ validation cadence, touch marking, and dirty tracking are identical to
65
+ manual entry — one write path, no programmatic special case.
66
+ - `min`/`max` flow to both the native input and the picker;
67
+ `DatePickerInput` adds `isDateDisabled`, `showWeekNumbers`,
68
+ `firstDayOfWeek`, and `locale` (all picker-side — constraints still
69
+ belong in the schema, typing can produce anything).
70
+ - The trigger's accessible name comes from `labels.calendar.open`; while
71
+ the form submits, the input turns readonly and the trigger disables.
72
+
46
73
  ## The validation model
47
74
 
48
75
  - **With `schema`** (a Standard Schema, e.g. valibot): validation runs
package/dist/index.d.ts CHANGED
@@ -9,10 +9,13 @@ export { default as Submit } from "./components/submit.svelte";
9
9
  export { default as Form } from "./form.svelte";
10
10
  export { default as CheckboxInput } from "./inputs/checkbox-input.svelte";
11
11
  export { default as DateInput } from "./inputs/date-input.svelte";
12
+ export { default as DatePickerInput } from "./inputs/date-picker-input.svelte";
13
+ export { default as MonthPickerInput } from "./inputs/month-picker-input.svelte";
12
14
  export { default as NumberInput } from "./inputs/number-input.svelte";
13
15
  export { default as SelectInput } from "./inputs/select-input.svelte";
14
16
  export { default as TextareaInput } from "./inputs/textarea-input.svelte";
15
17
  export { default as TextInput } from "./inputs/text-input.svelte";
18
+ export { default as WeekPickerInput } from "./inputs/week-picker-input.svelte";
16
19
  export { wireField } from "./inputs/wire-field";
17
20
  export type { WiredField, WireFieldOptions } from "./inputs/wire-field";
18
21
  export type { CheckboxField, CheckboxFieldAttributes, DateField, DateFieldAttributes, DateFieldType, FieldRegistration, NumberField, NumberFieldAttributes, SelectField, SelectFieldAttributes, TextField, TextFieldAttributes, TextFieldType, } from "./types/field";
package/dist/index.js CHANGED
@@ -9,9 +9,12 @@ export { default as Submit } from "./components/submit.svelte";
9
9
  export { default as Form } from "./form.svelte";
10
10
  export { default as CheckboxInput } from "./inputs/checkbox-input.svelte";
11
11
  export { default as DateInput } from "./inputs/date-input.svelte";
12
+ export { default as DatePickerInput } from "./inputs/date-picker-input.svelte";
13
+ export { default as MonthPickerInput } from "./inputs/month-picker-input.svelte";
12
14
  export { default as NumberInput } from "./inputs/number-input.svelte";
13
15
  export { default as SelectInput } from "./inputs/select-input.svelte";
14
16
  export { default as TextareaInput } from "./inputs/textarea-input.svelte";
15
17
  export { default as TextInput } from "./inputs/text-input.svelte";
18
+ export { default as WeekPickerInput } from "./inputs/week-picker-input.svelte";
16
19
  // The extension point for custom inputs.
17
20
  export { wireField } from "./inputs/wire-field";
@@ -0,0 +1,134 @@
1
+ <!-- @component
2
+ Date field with a cross-browser calendar popover, wired to a SvelteKit
3
+ remote form field — must render inside a <Form>. The native date input
4
+ stays visible as the FormData carrier (typeable, native mobile pickers
5
+ intact); the overlaid calendar button opens the library's DatePicker in
6
+ an anchored Popover, and a pick writes the input exactly the way typing
7
+ does — validation, touch, and dirty tracking behave identically. Values
8
+ are the native input's "YYYY-MM-DD" strings.
9
+
10
+ Firefox exception: it draws an unhideable calendar icon on date inputs
11
+ (Bugzilla 1830890), so there the native affordance wins — our trigger
12
+ hides and the icon opens Firefox's own date picker (min/max respected;
13
+ week numbers and isDateDisabled are picker-side visuals only there —
14
+ the schema validates regardless, as always).
15
+ -->
16
+ <script lang="ts">
17
+ import DatePicker from "@privaty/ui/calendar/date-picker.svelte";
18
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
19
+ import type { DateField } from "../types/field";
20
+ import PickerFrame from "./picker-frame.svelte";
21
+
22
+ interface Props {
23
+ /** The SvelteKit remote form field to bind. Structural — anything
24
+ * satisfying the DateField slice works, e.g. a fake from the public
25
+ * testing subpath. */
26
+ field: DateField;
27
+ /** Visible label text for the control. */
28
+ label: string;
29
+
30
+ /** Label placement: "top" (default), "left", or "hidden". No floating:
31
+ * date-family inputs render their format scaffold even when empty, so
32
+ * :placeholder-shown never behaves. */
33
+ labelStyle?: Exclude<LabelStyle, "floating">;
34
+
35
+ /** Marks the field required: feeds the majority-aware required/optional
36
+ * marker. Validation itself comes from the field's schema. */
37
+ required?: boolean;
38
+ /** Disables the control. Disabled controls are excluded from FormData —
39
+ * never disable to lock the form during submit; the input already turns
40
+ * readonly (and the calendar trigger disables) while submitting. */
41
+ disabled?: boolean;
42
+ /** Renders the input readonly and disables the calendar trigger. */
43
+ readonly?: boolean;
44
+
45
+ /** Initial "YYYY-MM-DD" value. */
46
+ initialValue?: string;
47
+
48
+ /** Inclusive ISO bounds, applied to BOTH the native input and the
49
+ * picker (out-of-range days render disabled). */
50
+ min?: string;
51
+ max?: string;
52
+ /** Marks additional days disabled in the picker (booked dates,
53
+ * weekends, …). Typing can still produce them — validate in the
54
+ * schema, as with every constraint. */
55
+ isDateDisabled?: (iso: string) => boolean;
56
+
57
+ /** BCP 47 tag for the picker's names and default week start —
58
+ * overrides `UiConfig.locale`. */
59
+ locale?: string;
60
+ /** ISO weekday the picker grid starts on (Monday = 1 … Sunday = 7). */
61
+ firstDayOfWeek?: number;
62
+ /** Adds the ISO week-number column to the picker. */
63
+ showWeekNumbers?: boolean;
64
+
65
+ /** Extra classes for the outer field wrapper. */
66
+ class?: string;
67
+ /** Extra classes for the <label> element. */
68
+ labelClass?: string;
69
+ /** Extra classes for the <input> element. */
70
+ inputClass?: string;
71
+ /** Extra classes for the required/optional marker. */
72
+ markerClass?: string;
73
+ /** Extra classes for the error <ul>. */
74
+ errorClass?: string;
75
+ }
76
+
77
+ const {
78
+ field,
79
+ label,
80
+
81
+ labelStyle,
82
+
83
+ required = false,
84
+ disabled = false,
85
+ readonly = false,
86
+
87
+ initialValue = "",
88
+
89
+ min,
90
+ max,
91
+ isDateDisabled,
92
+
93
+ locale,
94
+ firstDayOfWeek,
95
+ showWeekNumbers = false,
96
+
97
+ class: classes,
98
+ labelClass,
99
+ inputClass,
100
+ markerClass,
101
+ errorClass,
102
+ }: Props = $props();
103
+ </script>
104
+
105
+ <PickerFrame
106
+ {field}
107
+ {label}
108
+ type="date"
109
+ {labelStyle}
110
+ {required}
111
+ {disabled}
112
+ {readonly}
113
+ {initialValue}
114
+ {min}
115
+ {max}
116
+ class={classes}
117
+ {labelClass}
118
+ {inputClass}
119
+ {markerClass}
120
+ {errorClass}
121
+ >
122
+ {#snippet picker({ value, select })}
123
+ <DatePicker
124
+ {value}
125
+ onselect={select}
126
+ {min}
127
+ {max}
128
+ {isDateDisabled}
129
+ {locale}
130
+ {firstDayOfWeek}
131
+ {showWeekNumbers}
132
+ />
133
+ {/snippet}
134
+ </PickerFrame>
@@ -0,0 +1,68 @@
1
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
2
+ import type { DateField } from "../types/field";
3
+ interface Props {
4
+ /** The SvelteKit remote form field to bind. Structural — anything
5
+ * satisfying the DateField slice works, e.g. a fake from the public
6
+ * testing subpath. */
7
+ field: DateField;
8
+ /** Visible label text for the control. */
9
+ label: string;
10
+ /** Label placement: "top" (default), "left", or "hidden". No floating:
11
+ * date-family inputs render their format scaffold even when empty, so
12
+ * :placeholder-shown never behaves. */
13
+ labelStyle?: Exclude<LabelStyle, "floating">;
14
+ /** Marks the field required: feeds the majority-aware required/optional
15
+ * marker. Validation itself comes from the field's schema. */
16
+ required?: boolean;
17
+ /** Disables the control. Disabled controls are excluded from FormData —
18
+ * never disable to lock the form during submit; the input already turns
19
+ * readonly (and the calendar trigger disables) while submitting. */
20
+ disabled?: boolean;
21
+ /** Renders the input readonly and disables the calendar trigger. */
22
+ readonly?: boolean;
23
+ /** Initial "YYYY-MM-DD" value. */
24
+ initialValue?: string;
25
+ /** Inclusive ISO bounds, applied to BOTH the native input and the
26
+ * picker (out-of-range days render disabled). */
27
+ min?: string;
28
+ max?: string;
29
+ /** Marks additional days disabled in the picker (booked dates,
30
+ * weekends, …). Typing can still produce them — validate in the
31
+ * schema, as with every constraint. */
32
+ isDateDisabled?: (iso: string) => boolean;
33
+ /** BCP 47 tag for the picker's names and default week start —
34
+ * overrides `UiConfig.locale`. */
35
+ locale?: string;
36
+ /** ISO weekday the picker grid starts on (Monday = 1 … Sunday = 7). */
37
+ firstDayOfWeek?: number;
38
+ /** Adds the ISO week-number column to the picker. */
39
+ showWeekNumbers?: boolean;
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 field with a cross-browser calendar popover, wired to a SvelteKit
53
+ * remote form field — must render inside a <Form>. The native date input
54
+ * stays visible as the FormData carrier (typeable, native mobile pickers
55
+ * intact); the overlaid calendar button opens the library's DatePicker in
56
+ * an anchored Popover, and a pick writes the input exactly the way typing
57
+ * does — validation, touch, and dirty tracking behave identically. Values
58
+ * are the native input's "YYYY-MM-DD" strings.
59
+ *
60
+ * Firefox exception: it draws an unhideable calendar icon on date inputs
61
+ * (Bugzilla 1830890), so there the native affordance wins — our trigger
62
+ * hides and the icon opens Firefox's own date picker (min/max respected;
63
+ * week numbers and isDateDisabled are picker-side visuals only there —
64
+ * the schema validates regardless, as always).
65
+ */
66
+ declare const DatePickerInput: import("svelte").Component<Props, {}, "">;
67
+ type DatePickerInput = ReturnType<typeof DatePickerInput>;
68
+ export default DatePickerInput;
@@ -0,0 +1,107 @@
1
+ <!-- @component
2
+ Month field with a cross-browser picker popover, wired to a SvelteKit
3
+ remote form field — must render inside a <Form>. The native month input
4
+ stays visible as the FormData carrier (Firefox renders it as a plain
5
+ text input — exactly the gap the picker fills); the overlaid calendar
6
+ button opens the library's MonthPicker in an anchored Popover, and a
7
+ pick writes the input exactly the way typing does. Values are the
8
+ native input's "YYYY-MM" strings.
9
+ -->
10
+ <script lang="ts">
11
+ import MonthPicker from "@privaty/ui/calendar/month-picker.svelte";
12
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
13
+ import type { DateField } from "../types/field";
14
+ import PickerFrame from "./picker-frame.svelte";
15
+
16
+ interface Props {
17
+ /** The SvelteKit remote form field to bind. Structural — anything
18
+ * satisfying the DateField slice works, e.g. a fake from the public
19
+ * testing subpath. */
20
+ field: DateField;
21
+ /** Visible label text for the control. */
22
+ label: string;
23
+
24
+ /** Label placement: "top" (default), "left", or "hidden". No floating
25
+ * (see DateInput). */
26
+ labelStyle?: Exclude<LabelStyle, "floating">;
27
+
28
+ /** Marks the field required: feeds the majority-aware required/optional
29
+ * marker. Validation itself comes from the field's schema. */
30
+ required?: boolean;
31
+ /** Disables the control. Disabled controls are excluded from FormData —
32
+ * never disable to lock the form during submit; the input already turns
33
+ * readonly (and the calendar trigger disables) while submitting. */
34
+ disabled?: boolean;
35
+ /** Renders the input readonly and disables the calendar trigger. */
36
+ readonly?: boolean;
37
+
38
+ /** Initial "YYYY-MM" value. */
39
+ initialValue?: string;
40
+
41
+ /** Inclusive "YYYY-MM" bounds, applied to BOTH the native input and
42
+ * the picker (out-of-range months render disabled). */
43
+ min?: string;
44
+ max?: string;
45
+
46
+ /** BCP 47 tag for the picker's month names — overrides
47
+ * `UiConfig.locale`. */
48
+ locale?: string;
49
+
50
+ /** Extra classes for the outer field wrapper. */
51
+ class?: string;
52
+ /** Extra classes for the <label> element. */
53
+ labelClass?: string;
54
+ /** Extra classes for the <input> element. */
55
+ inputClass?: string;
56
+ /** Extra classes for the required/optional marker. */
57
+ markerClass?: string;
58
+ /** Extra classes for the error <ul>. */
59
+ errorClass?: string;
60
+ }
61
+
62
+ const {
63
+ field,
64
+ label,
65
+
66
+ labelStyle,
67
+
68
+ required = false,
69
+ disabled = false,
70
+ readonly = false,
71
+
72
+ initialValue = "",
73
+
74
+ min,
75
+ max,
76
+
77
+ locale,
78
+
79
+ class: classes,
80
+ labelClass,
81
+ inputClass,
82
+ markerClass,
83
+ errorClass,
84
+ }: Props = $props();
85
+ </script>
86
+
87
+ <PickerFrame
88
+ {field}
89
+ {label}
90
+ type="month"
91
+ {labelStyle}
92
+ {required}
93
+ {disabled}
94
+ {readonly}
95
+ {initialValue}
96
+ {min}
97
+ {max}
98
+ class={classes}
99
+ {labelClass}
100
+ {inputClass}
101
+ {markerClass}
102
+ {errorClass}
103
+ >
104
+ {#snippet picker({ value, select })}
105
+ <MonthPicker {value} onselect={select} {min} {max} {locale} />
106
+ {/snippet}
107
+ </PickerFrame>
@@ -0,0 +1,53 @@
1
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
2
+ import type { DateField } from "../types/field";
3
+ interface Props {
4
+ /** The SvelteKit remote form field to bind. Structural — anything
5
+ * satisfying the DateField slice works, e.g. a fake from the public
6
+ * testing subpath. */
7
+ field: DateField;
8
+ /** Visible label text for the control. */
9
+ label: string;
10
+ /** Label placement: "top" (default), "left", or "hidden". No floating
11
+ * (see DateInput). */
12
+ labelStyle?: Exclude<LabelStyle, "floating">;
13
+ /** Marks the field required: feeds the majority-aware required/optional
14
+ * marker. Validation itself comes from the field's schema. */
15
+ required?: boolean;
16
+ /** Disables the control. Disabled controls are excluded from FormData —
17
+ * never disable to lock the form during submit; the input already turns
18
+ * readonly (and the calendar trigger disables) while submitting. */
19
+ disabled?: boolean;
20
+ /** Renders the input readonly and disables the calendar trigger. */
21
+ readonly?: boolean;
22
+ /** Initial "YYYY-MM" value. */
23
+ initialValue?: string;
24
+ /** Inclusive "YYYY-MM" bounds, applied to BOTH the native input and
25
+ * the picker (out-of-range months render disabled). */
26
+ min?: string;
27
+ max?: string;
28
+ /** BCP 47 tag for the picker's month names — overrides
29
+ * `UiConfig.locale`. */
30
+ locale?: string;
31
+ /** Extra classes for the outer field wrapper. */
32
+ class?: string;
33
+ /** Extra classes for the <label> element. */
34
+ labelClass?: string;
35
+ /** Extra classes for the <input> element. */
36
+ inputClass?: string;
37
+ /** Extra classes for the required/optional marker. */
38
+ markerClass?: string;
39
+ /** Extra classes for the error <ul>. */
40
+ errorClass?: string;
41
+ }
42
+ /**
43
+ * Month field with a cross-browser picker popover, wired to a SvelteKit
44
+ * remote form field — must render inside a <Form>. The native month input
45
+ * stays visible as the FormData carrier (Firefox renders it as a plain
46
+ * text input — exactly the gap the picker fills); the overlaid calendar
47
+ * button opens the library's MonthPicker in an anchored Popover, and a
48
+ * pick writes the input exactly the way typing does. Values are the
49
+ * native input's "YYYY-MM" strings.
50
+ */
51
+ declare const MonthPickerInput: import("svelte").Component<Props, {}, "">;
52
+ type MonthPickerInput = ReturnType<typeof MonthPickerInput>;
53
+ export default MonthPickerInput;
@@ -0,0 +1,199 @@
1
+ <!-- @component
2
+ INTERNAL shared frame for the picker inputs (DatePickerInput,
3
+ MonthPickerInput, WeekPickerInput) — not exported. A real native input
4
+ wired to a SvelteKit remote form field stays the FormData carrier
5
+ (typeable, native mobile pickers intact), with a calendar trigger button
6
+ overlaid on its end that opens the given picker in an anchored Popover.
7
+
8
+ A pick writes the input THE WAY TYPING DOES: DOM value first, then a real
9
+ bubbling `input` event. Kit's form-level listener reads the element's
10
+ value into field state, and the Form validates and marks the field
11
+ touched in exactly the order manual entry would — one write path, no
12
+ special programmatic branch.
13
+ -->
14
+ <script lang="ts">
15
+ import { tick, type Snippet } from "svelte";
16
+ import { CalendarIcon } from "@lucide/svelte";
17
+ import { cn } from "@privaty/ui/cn.js";
18
+ import FieldFrame from "@privaty/ui/components/field-frame.svelte";
19
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
20
+ import { getUiConfig } from "@privaty/ui/config/context.js";
21
+ import { getUiDensity } from "@privaty/ui/config/density.js";
22
+ import Popover from "@privaty/ui/overlays/popover.svelte";
23
+ import { coreTheme } from "@privaty/ui/theme.js";
24
+ import type { DateField, DateFieldType } from "../types/field";
25
+ import { wireField } from "./wire-field";
26
+
27
+ interface Props {
28
+ /** The SvelteKit remote form field to bind (structural slice). */
29
+ field: DateField;
30
+ /** Visible label text for the control. */
31
+ label: string;
32
+ /** The native carrier type — also the value format the picker speaks. */
33
+ type: Extract<DateFieldType, "date" | "month" | "week">;
34
+
35
+ /** Label placement — no floating (see DateInput). */
36
+ labelStyle?: Exclude<LabelStyle, "floating">;
37
+
38
+ required?: boolean;
39
+ disabled?: boolean;
40
+ readonly?: boolean;
41
+
42
+ /** ISO-style string in the chosen type's format. */
43
+ initialValue?: string;
44
+
45
+ /** Native bounds, forwarded to the input (the wrappers also forward
46
+ * them to their picker). */
47
+ min?: string;
48
+ max?: string;
49
+
50
+ class?: string;
51
+ labelClass?: string;
52
+ inputClass?: string;
53
+ markerClass?: string;
54
+ errorClass?: string;
55
+
56
+ /** Renders the picker inside the Popover panel. `value` is the field's
57
+ * current string; wire `select` to the picker's `onselect`. */
58
+ picker: Snippet<[{ value: string; select: (iso: string) => void }]>;
59
+ }
60
+
61
+ const {
62
+ field,
63
+ label,
64
+ type,
65
+
66
+ labelStyle,
67
+
68
+ required = false,
69
+ disabled = false,
70
+ readonly = false,
71
+
72
+ initialValue = "",
73
+
74
+ min,
75
+ max,
76
+
77
+ class: classes,
78
+ labelClass,
79
+ inputClass,
80
+ markerClass,
81
+ errorClass,
82
+
83
+ picker,
84
+ }: Props = $props();
85
+
86
+ const config = getUiConfig();
87
+
88
+ const attributes = $derived(field.as(type, initialValue));
89
+
90
+ // The field, type, and initialValue are stable for the component's lifetime,
91
+ // so capturing the initial name and registration is intentional.
92
+ // svelte-ignore state_referenced_locally
93
+ const name = attributes.name;
94
+
95
+ // svelte-ignore state_referenced_locally
96
+ const wired = wireField({
97
+ name,
98
+ initialValue,
99
+ required,
100
+ issues: () => field.issues(),
101
+ getValue: () => field.value(),
102
+ setValue: (value) => field.set(value as string),
103
+ normalize: (value) => (value == null ? "" : String(value)),
104
+ });
105
+
106
+ const densityContext = getUiDensity();
107
+ const compact = $derived(densityContext.density === "compact");
108
+
109
+ // What the picker highlights: the field's live value (Kit tracks
110
+ // undefined until the first edit — the seed stands in).
111
+ const currentValue = $derived(String(field.value() ?? initialValue));
112
+
113
+ const uid = $props.id();
114
+ const locked = $derived(disabled || readonly || wired.state.isSubmitting);
115
+
116
+ let inputElement = $state<HTMLInputElement>();
117
+ let triggerElement = $state<HTMLButtonElement>();
118
+ let open = $state(false);
119
+
120
+ function select(iso: string) {
121
+ // The popover can outlive its trigger's enabled state (a submit that
122
+ // starts while it is open) — a locked control accepts no picks.
123
+ if (!inputElement || locked) return;
124
+ inputElement.value = iso;
125
+ // A real bubbling event — Kit's and the Form's listeners sit on the
126
+ // <form>, and Svelte delegates besides.
127
+ inputElement.dispatchEvent(new Event("input", { bubbles: true }));
128
+ open = false;
129
+ // The focused cell just left the top layer with the panel — hand focus
130
+ // back to the trigger, the way light dismiss does.
131
+ void tick().then(() => triggerElement?.focus());
132
+ }
133
+ </script>
134
+
135
+ <FieldFrame
136
+ id={uid}
137
+ {label}
138
+ {labelStyle}
139
+ errors={wired.errors}
140
+ marker={wired.marker}
141
+ class={classes}
142
+ {labelClass}
143
+ markerClass={cn(required && "text-red-700 dark:text-red-500", markerClass)}
144
+ {errorClass}
145
+ >
146
+ {#snippet control({ id, errorsId })}
147
+ <span class="relative block w-full">
148
+ <input
149
+ {...attributes}
150
+ bind:this={inputElement}
151
+ {id}
152
+ class={cn(
153
+ coreTheme.controlBase,
154
+ coreTheme.controlSurface,
155
+ compact
156
+ ? coreTheme.controlPadding.compact
157
+ : coreTheme.controlPadding.comfortable,
158
+ // Our trigger replaces Chromium's built-in indicator; pr-8 keeps
159
+ // the text clear of the overlaid button.
160
+ "pr-8 [&::-webkit-calendar-picker-indicator]:hidden",
161
+ // Firefox refuses to hide its own date icon (Bugzilla 1830890) —
162
+ // there the native affordance wins for dates: our trigger steps
163
+ // aside (below) and the padding returns to base. The @supports
164
+ // (-moz-appearance) detect matches Gecko only. Month/week draw
165
+ // no native icon anywhere, so they keep our trigger everywhere.
166
+ type === "date" && "supports-[-moz-appearance:none]:pr-2",
167
+ inputClass,
168
+ )}
169
+ aria-describedby={errorsId}
170
+ aria-invalid={wired.errors.length > 0 ? true : undefined}
171
+ {disabled}
172
+ readonly={readonly || wired.state.isSubmitting}
173
+ {min}
174
+ {max}
175
+ />
176
+ <Popover bind:open placement="bottom-end">
177
+ {#snippet trigger(props)}
178
+ <button
179
+ type="button"
180
+ bind:this={triggerElement}
181
+ {...props}
182
+ class={cn(
183
+ coreTheme.controlTrigger,
184
+ // See the input's class note: Firefox dates use the native
185
+ // icon and picker instead of a doubled affordance.
186
+ type === "date" && "supports-[-moz-appearance:none]:hidden",
187
+ )}
188
+ disabled={locked}
189
+ title={config.labels.calendar.open}
190
+ >
191
+ <CalendarIcon class="size-4" aria-hidden="true" />
192
+ <span class="sr-only">{config.labels.calendar.open}</span>
193
+ </button>
194
+ {/snippet}
195
+ {@render picker({ value: currentValue, select })}
196
+ </Popover>
197
+ </span>
198
+ {/snippet}
199
+ </FieldFrame>
@@ -0,0 +1,49 @@
1
+ import { type Snippet } from "svelte";
2
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
3
+ import type { DateField, DateFieldType } from "../types/field";
4
+ interface Props {
5
+ /** The SvelteKit remote form field to bind (structural slice). */
6
+ field: DateField;
7
+ /** Visible label text for the control. */
8
+ label: string;
9
+ /** The native carrier type — also the value format the picker speaks. */
10
+ type: Extract<DateFieldType, "date" | "month" | "week">;
11
+ /** Label placement — no floating (see DateInput). */
12
+ labelStyle?: Exclude<LabelStyle, "floating">;
13
+ required?: boolean;
14
+ disabled?: boolean;
15
+ readonly?: boolean;
16
+ /** ISO-style string in the chosen type's format. */
17
+ initialValue?: string;
18
+ /** Native bounds, forwarded to the input (the wrappers also forward
19
+ * them to their picker). */
20
+ min?: string;
21
+ max?: string;
22
+ class?: string;
23
+ labelClass?: string;
24
+ inputClass?: string;
25
+ markerClass?: string;
26
+ errorClass?: string;
27
+ /** Renders the picker inside the Popover panel. `value` is the field's
28
+ * current string; wire `select` to the picker's `onselect`. */
29
+ picker: Snippet<[{
30
+ value: string;
31
+ select: (iso: string) => void;
32
+ }]>;
33
+ }
34
+ /**
35
+ * INTERNAL shared frame for the picker inputs (DatePickerInput,
36
+ * MonthPickerInput, WeekPickerInput) — not exported. A real native input
37
+ * wired to a SvelteKit remote form field stays the FormData carrier
38
+ * (typeable, native mobile pickers intact), with a calendar trigger button
39
+ * overlaid on its end that opens the given picker in an anchored Popover.
40
+ *
41
+ * A pick writes the input THE WAY TYPING DOES: DOM value first, then a real
42
+ * bubbling `input` event. Kit's form-level listener reads the element's
43
+ * value into field state, and the Form validates and marks the field
44
+ * touched in exactly the order manual entry would — one write path, no
45
+ * special programmatic branch.
46
+ */
47
+ declare const PickerFrame: import("svelte").Component<Props, {}, "">;
48
+ type PickerFrame = ReturnType<typeof PickerFrame>;
49
+ export default PickerFrame;
@@ -0,0 +1,108 @@
1
+ <!-- @component
2
+ Week field with a cross-browser picker popover, wired to a SvelteKit
3
+ remote form field — must render inside a <Form>. The native week input
4
+ stays visible as the FormData carrier (Firefox renders it as a plain
5
+ text input — exactly the gap the picker fills); the overlaid calendar
6
+ button opens the library's WeekPicker in an anchored Popover, and a pick
7
+ writes the input exactly the way typing does. Values are the native
8
+ input's "YYYY-Www" strings (ISO-8601 weeks, the Danish convention).
9
+ -->
10
+ <script lang="ts">
11
+ import WeekPicker from "@privaty/ui/calendar/week-picker.svelte";
12
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
13
+ import type { DateField } from "../types/field";
14
+ import PickerFrame from "./picker-frame.svelte";
15
+
16
+ interface Props {
17
+ /** The SvelteKit remote form field to bind. Structural — anything
18
+ * satisfying the DateField slice works, e.g. a fake from the public
19
+ * testing subpath. */
20
+ field: DateField;
21
+ /** Visible label text for the control. */
22
+ label: string;
23
+
24
+ /** Label placement: "top" (default), "left", or "hidden". No floating
25
+ * (see DateInput). */
26
+ labelStyle?: Exclude<LabelStyle, "floating">;
27
+
28
+ /** Marks the field required: feeds the majority-aware required/optional
29
+ * marker. Validation itself comes from the field's schema. */
30
+ required?: boolean;
31
+ /** Disables the control. Disabled controls are excluded from FormData —
32
+ * never disable to lock the form during submit; the input already turns
33
+ * readonly (and the calendar trigger disables) while submitting. */
34
+ disabled?: boolean;
35
+ /** Renders the input readonly and disables the calendar trigger. */
36
+ readonly?: boolean;
37
+
38
+ /** Initial "YYYY-Www" value. */
39
+ initialValue?: string;
40
+
41
+ /** Inclusive "YYYY-Www" bounds, applied to BOTH the native input and
42
+ * the picker (out-of-range weeks render disabled). */
43
+ min?: string;
44
+ max?: string;
45
+
46
+ /** BCP 47 tag for the picker's names — overrides `UiConfig.locale`.
47
+ * The grid itself is always Monday-first (ISO weeks are only
48
+ * well-defined that way). */
49
+ locale?: string;
50
+
51
+ /** Extra classes for the outer field wrapper. */
52
+ class?: string;
53
+ /** Extra classes for the <label> element. */
54
+ labelClass?: string;
55
+ /** Extra classes for the <input> element. */
56
+ inputClass?: string;
57
+ /** Extra classes for the required/optional marker. */
58
+ markerClass?: string;
59
+ /** Extra classes for the error <ul>. */
60
+ errorClass?: string;
61
+ }
62
+
63
+ const {
64
+ field,
65
+ label,
66
+
67
+ labelStyle,
68
+
69
+ required = false,
70
+ disabled = false,
71
+ readonly = false,
72
+
73
+ initialValue = "",
74
+
75
+ min,
76
+ max,
77
+
78
+ locale,
79
+
80
+ class: classes,
81
+ labelClass,
82
+ inputClass,
83
+ markerClass,
84
+ errorClass,
85
+ }: Props = $props();
86
+ </script>
87
+
88
+ <PickerFrame
89
+ {field}
90
+ {label}
91
+ type="week"
92
+ {labelStyle}
93
+ {required}
94
+ {disabled}
95
+ {readonly}
96
+ {initialValue}
97
+ {min}
98
+ {max}
99
+ class={classes}
100
+ {labelClass}
101
+ {inputClass}
102
+ {markerClass}
103
+ {errorClass}
104
+ >
105
+ {#snippet picker({ value, select })}
106
+ <WeekPicker {value} onselect={select} {min} {max} {locale} />
107
+ {/snippet}
108
+ </PickerFrame>
@@ -0,0 +1,54 @@
1
+ import type { LabelStyle } from "@privaty/ui/components/types.js";
2
+ import type { DateField } from "../types/field";
3
+ interface Props {
4
+ /** The SvelteKit remote form field to bind. Structural — anything
5
+ * satisfying the DateField slice works, e.g. a fake from the public
6
+ * testing subpath. */
7
+ field: DateField;
8
+ /** Visible label text for the control. */
9
+ label: string;
10
+ /** Label placement: "top" (default), "left", or "hidden". No floating
11
+ * (see DateInput). */
12
+ labelStyle?: Exclude<LabelStyle, "floating">;
13
+ /** Marks the field required: feeds the majority-aware required/optional
14
+ * marker. Validation itself comes from the field's schema. */
15
+ required?: boolean;
16
+ /** Disables the control. Disabled controls are excluded from FormData —
17
+ * never disable to lock the form during submit; the input already turns
18
+ * readonly (and the calendar trigger disables) while submitting. */
19
+ disabled?: boolean;
20
+ /** Renders the input readonly and disables the calendar trigger. */
21
+ readonly?: boolean;
22
+ /** Initial "YYYY-Www" value. */
23
+ initialValue?: string;
24
+ /** Inclusive "YYYY-Www" bounds, applied to BOTH the native input and
25
+ * the picker (out-of-range weeks render disabled). */
26
+ min?: string;
27
+ max?: string;
28
+ /** BCP 47 tag for the picker's names — overrides `UiConfig.locale`.
29
+ * The grid itself is always Monday-first (ISO weeks are only
30
+ * well-defined that way). */
31
+ locale?: string;
32
+ /** Extra classes for the outer field wrapper. */
33
+ class?: string;
34
+ /** Extra classes for the <label> element. */
35
+ labelClass?: string;
36
+ /** Extra classes for the <input> element. */
37
+ inputClass?: string;
38
+ /** Extra classes for the required/optional marker. */
39
+ markerClass?: string;
40
+ /** Extra classes for the error <ul>. */
41
+ errorClass?: string;
42
+ }
43
+ /**
44
+ * Week field with a cross-browser picker popover, wired to a SvelteKit
45
+ * remote form field — must render inside a <Form>. The native week input
46
+ * stays visible as the FormData carrier (Firefox renders it as a plain
47
+ * text input — exactly the gap the picker fills); the overlaid calendar
48
+ * button opens the library's WeekPicker in an anchored Popover, and a pick
49
+ * writes the input exactly the way typing does. Values are the native
50
+ * input's "YYYY-Www" strings (ISO-8601 weeks, the Danish convention).
51
+ */
52
+ declare const WeekPickerInput: import("svelte").Component<Props, {}, "">;
53
+ type WeekPickerInput = ReturnType<typeof WeekPickerInput>;
54
+ export default WeekPickerInput;
@@ -0,0 +1,5 @@
1
+ /* Loads Tailwind into browser-mode specs that assert real computed styles —
2
+ component classes are inert without it (e.g. the picker trigger's
3
+ Firefox-only display:none never applies). Same recipe as @privaty/ui's
4
+ testing stylesheet: v4 auto-detects sources from the repo root. */
5
+ @import "tailwindcss";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privaty/ui-forms",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,9 +41,10 @@
41
41
  "peerDependencies": {
42
42
  "@sveltejs/kit": "^3.0.0-next.0",
43
43
  "svelte": "^5.0.0",
44
- "@privaty/ui": "^0.1.5"
44
+ "@privaty/ui": "^0.2.0"
45
45
  },
46
46
  "dependencies": {
47
+ "@lucide/svelte": "^1.34.0",
47
48
  "@standard-schema/spec": "^1.1.0"
48
49
  },
49
50
  "devDependencies": {
@@ -55,8 +56,8 @@
55
56
  "svelte-check": "^4.7.6",
56
57
  "typescript": "^6.0.3",
57
58
  "@config/eslint": "0.0.0",
58
- "@privaty/ui": "0.1.5",
59
- "@config/typescript": "0.0.0"
59
+ "@config/typescript": "0.0.0",
60
+ "@privaty/ui": "0.2.0"
60
61
  },
61
62
  "scripts": {
62
63
  "check": "svelte-check --tsconfig ./tsconfig.json",