@marianmeres/stuic 3.170.0 → 3.172.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.
Files changed (59) hide show
  1. package/AGENTS.md +10 -8
  2. package/API.md +90 -0
  3. package/README.md +1 -1
  4. package/dist/components/AssetsPreview/AssetsPreview.fixture.svelte +1 -1
  5. package/dist/components/Calendar/Calendar.svelte +799 -0
  6. package/dist/components/Calendar/Calendar.svelte.d.ts +135 -0
  7. package/dist/components/Calendar/README.md +294 -0
  8. package/dist/components/Calendar/calendar-i18n-sk.d.ts +20 -0
  9. package/dist/components/Calendar/calendar-i18n-sk.js +46 -0
  10. package/dist/components/Calendar/calendar-i18n.d.ts +61 -0
  11. package/dist/components/Calendar/calendar-i18n.js +73 -0
  12. package/dist/components/Calendar/index.css +307 -0
  13. package/dist/components/Calendar/index.d.ts +5 -0
  14. package/dist/components/Calendar/index.js +5 -0
  15. package/dist/components/Calendar/iso-date.d.ts +107 -0
  16. package/dist/components/Calendar/iso-date.js +247 -0
  17. package/dist/components/CommandMenu/CommandMenu.fixture.svelte +1 -1
  18. package/dist/components/ContextMenu/ContextMenu.svelte +1 -1
  19. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +1 -1
  20. package/dist/components/ContextMenu/README.md +1 -1
  21. package/dist/components/DropdownMenu/DropdownMenu.svelte +57 -3
  22. package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +4 -2
  23. package/dist/components/DropdownMenu/README.md +1 -0
  24. package/dist/components/Input/FieldDate.svelte +349 -0
  25. package/dist/components/Input/FieldDate.svelte.d.ts +79 -0
  26. package/dist/components/Input/FieldDateRange.svelte +373 -0
  27. package/dist/components/Input/FieldDateRange.svelte.d.ts +90 -0
  28. package/dist/components/Input/README.md +149 -16
  29. package/dist/components/Input/_internal/FieldDateShell.svelte +327 -0
  30. package/dist/components/Input/_internal/FieldDateShell.svelte.d.ts +66 -0
  31. package/dist/components/Input/index.css +119 -0
  32. package/dist/components/Input/index.d.ts +2 -0
  33. package/dist/components/Input/index.js +2 -0
  34. package/dist/components/ModalDialog/ModalDialog.fixture.svelte +1 -1
  35. package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +1 -1
  36. package/dist/icons/index.d.ts +1 -0
  37. package/dist/icons/index.js +1 -0
  38. package/dist/index.css +1 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +1 -0
  41. package/docs/_archive/README.md +10 -0
  42. package/docs/{component-testing → _archive/component-testing}/00-overview-and-roadmap.md +8 -6
  43. package/docs/{component-testing → _archive/component-testing}/01-framework-setup.md +4 -2
  44. package/docs/{component-testing → _archive/component-testing}/03-component-coverage-roadmap.md +3 -1
  45. package/docs/{component-testing → _archive/component-testing}/04-hard-cases-and-e2e.md +5 -3
  46. package/docs/{component-testing → _archive/component-testing}/05-ci.md +2 -0
  47. package/docs/{component-testing → _archive/component-testing}/PROGRESS.md +3 -1
  48. package/docs/_archive/component-testing/README.md +28 -0
  49. package/docs/{upgrading.md → _archive/upgrading.md} +2 -0
  50. package/docs/architecture.md +19 -11
  51. package/docs/domains/actions.md +4 -3
  52. package/docs/domains/components.md +22 -19
  53. package/docs/domains/utils.md +2 -1
  54. package/docs/maybe-todo.md +6 -6
  55. package/docs/tasks.md +19 -9
  56. package/docs/{component-testing/02-test-conventions.md → testing-components.md} +30 -31
  57. package/docs/testing.md +3 -3
  58. package/package.json +2 -1
  59. package/docs/component-testing/README.md +0 -38
@@ -0,0 +1,373 @@
1
+ <!--
2
+ FieldDateRange — pick an inclusive date range (`start` … `end`).
3
+
4
+ Both bound ends are ISO `YYYY-MM-DD` strings (or null). The user picks them in
5
+ a range-mode `Calendar` — first pick anchors, second completes, order-agnostic —
6
+ in a dialog opened from a trigger showing the formatted range (default), or
7
+ inline inside the field (`embedded`). Two hidden inputs (`nameStart`, `nameEnd`)
8
+ carry the ISO values into FormData.
9
+
10
+ A half range (start without end) is displayed ("Sep 1, 2026 – …") but fails
11
+ validation, whether or not the field is `required`.
12
+ -->
13
+ <script lang="ts" module>
14
+ import type { Snippet } from "svelte";
15
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
16
+ import type { TranslateFn } from "../../types.js";
17
+ import type { THC } from "../Thc/Thc.svelte";
18
+ import type { Props as CalendarProps } from "../Calendar/Calendar.svelte";
19
+ import type { IsoDate } from "../Calendar/iso-date.js";
20
+ import type { FieldDateCalendarProps } from "./FieldDate.svelte";
21
+ import type { InputWrapClassProps } from "./types.js";
22
+
23
+ type SnippetWithId = Snippet<[{ id: string }]>;
24
+
25
+ /** Payload of `onChange`. */
26
+ export interface FieldDateRangeValue {
27
+ start: IsoDate | null;
28
+ end: IsoDate | null;
29
+ }
30
+
31
+ export interface Props extends InputWrapClassProps, FieldDateCalendarProps {
32
+ /** Range start as `YYYY-MM-DD` (bindable). */
33
+ start?: IsoDate | null;
34
+ /** Range end as `YYYY-MM-DD` (bindable). */
35
+ end?: IsoDate | null;
36
+ /** Form field name of the hidden input carrying `start`. */
37
+ nameStart?: string;
38
+ /** Form field name of the hidden input carrying `end`. */
39
+ nameEnd?: string;
40
+ /** Render the calendar inline inside the field instead of a trigger + dialog. */
41
+ embedded?: boolean;
42
+ /** Close the dialog once the range is complete. Default `true`. */
43
+ closeOnSelect?: boolean;
44
+ /** Trigger text while empty. Defaults to `t("placeholder_range")`. */
45
+ placeholder?: string;
46
+ /** Offer clearing (trailing × on the trigger / "Clear" in the embedded footer). Default `true`. */
47
+ clearable?: boolean;
48
+ /** BCP 47 locale of the display text and the calendar names. Browser default when omitted. */
49
+ locale?: string;
50
+ /** `Intl.DateTimeFormat` options of the display text. Default `{ dateStyle: "medium" }`. */
51
+ formatOptions?: Intl.DateTimeFormatOptions;
52
+ /** Custom display formatter for a complete range (wins over `formatOptions`). */
53
+ format?: (start: IsoDate, end: IsoDate) => string;
54
+ /**
55
+ * Months the calendar shows side by side at the `md` breakpoint and up (below
56
+ * it always a single month). Default `2`.
57
+ */
58
+ months?: number;
59
+ /** Anything else for the inner `Calendar` (class hooks, `view`, …). Applied last. */
60
+ calendarProps?: Partial<CalendarProps>;
61
+ /** The first (start) hidden input (bindable). */
62
+ input?: HTMLInputElement;
63
+ label?: SnippetWithId | THC;
64
+ description?: SnippetWithId | THC;
65
+ labelAfter?: SnippetWithId | THC;
66
+ below?: SnippetWithId | THC;
67
+ class?: string;
68
+ id?: string;
69
+ tabindex?: number;
70
+ renderSize?: "sm" | "md" | "lg" | string;
71
+ required?: boolean;
72
+ disabled?: boolean;
73
+ /**
74
+ * Validation. `false` disables the built-in guard (required / complete range /
75
+ * valid ISO / min / max / disabled dates); an options object is merged with it
76
+ * (your `customValidator` runs after the built-in checks pass). Default: the guard.
77
+ */
78
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
79
+ labelLeft?: boolean;
80
+ labelLeftWidth?: "normal" | "wide";
81
+ labelLeftBreakpoint?: number;
82
+ /** Classes for the trigger button */
83
+ classInput?: string;
84
+ /** Classes for the dialog box */
85
+ classDialog?: string;
86
+ style?: string;
87
+ noScrollLock?: boolean;
88
+ /** i18n — see `createCalendarT`. */
89
+ t?: TranslateFn;
90
+ /** Called after either end changed through the picker or the × (not on external writes). */
91
+ onChange?: (range: FieldDateRangeValue) => void;
92
+ }
93
+ </script>
94
+
95
+ <script lang="ts">
96
+ import { tick } from "svelte";
97
+ import type { ValidationResult } from "../../actions/validate.svelte.js";
98
+ import { Breakpoint } from "../../utils/breakpoint.svelte.js";
99
+ import { getId } from "../../utils/get-id.js";
100
+ import Button from "../Button/Button.svelte";
101
+ import Calendar, { type CalendarRangeChange } from "../Calendar/Calendar.svelte";
102
+ import { t_default } from "../Calendar/calendar-i18n.js";
103
+ import {
104
+ compareIso,
105
+ dayCellOf,
106
+ DEFAULT_DATE_FORMAT,
107
+ formatIsoDate,
108
+ formatIsoDateRange,
109
+ normalizeIsoDate,
110
+ } from "../Calendar/iso-date.js";
111
+ import FieldDateShell from "./_internal/FieldDateShell.svelte";
112
+
113
+ let {
114
+ start = $bindable(null),
115
+ end = $bindable(null),
116
+ nameStart,
117
+ nameEnd,
118
+ embedded = false,
119
+ closeOnSelect = true,
120
+ placeholder,
121
+ clearable = true,
122
+ locale,
123
+ formatOptions,
124
+ format,
125
+ months = 2,
126
+ calendarProps,
127
+ input = $bindable(),
128
+ label = "",
129
+ description,
130
+ labelAfter,
131
+ below,
132
+ class: classProp,
133
+ id = getId(),
134
+ tabindex = 0,
135
+ renderSize = "md",
136
+ required = false,
137
+ disabled = false,
138
+ // Renamed local binding to avoid collision with `export function validate()` below.
139
+ validate: validateProp,
140
+ labelLeft = false,
141
+ labelLeftWidth = "normal",
142
+ labelLeftBreakpoint = 480,
143
+ classInput,
144
+ classDialog,
145
+ style,
146
+ noScrollLock = false,
147
+ t = t_default,
148
+ onChange,
149
+ // calendar
150
+ min,
151
+ max,
152
+ isDateDisabled,
153
+ weekStartsOn,
154
+ weekendDays,
155
+ zone,
156
+ captionLayout,
157
+ yearRange,
158
+ showWeekNumbers,
159
+ showOutsideDays,
160
+ fixedWeeks,
161
+ showToday,
162
+ renderDay,
163
+ // wrapper class props
164
+ classLabel,
165
+ classLabelBox,
166
+ classInputBox,
167
+ classInputBoxWrap,
168
+ classInputBoxWrapInvalid,
169
+ classDescBox,
170
+ classDescBoxToggle,
171
+ classBelowBox,
172
+ classValidationBox,
173
+ }: Props = $props();
174
+
175
+ let shell: FieldDateShell | undefined = $state();
176
+
177
+ const bp = Breakpoint.instance;
178
+ // two months only fit from the md breakpoint up; phones always get one
179
+ let effectiveMonths = $derived(bp.md ? months : 1);
180
+
181
+ let _start = $derived(normalizeIsoDate(start));
182
+ let _end = $derived(normalizeIsoDate(end));
183
+ let _min = $derived(normalizeIsoDate(min));
184
+ let _max = $derived(normalizeIsoDate(max));
185
+
186
+ // Submitted values: the normalized dates, or the raw (unparseable) input so the
187
+ // validator can flag it instead of silently submitting "".
188
+ const raw = (v: IsoDate | null | undefined) => (v == null ? "" : `${v}`);
189
+ let submitStart = $derived(_start ?? raw(start));
190
+ let submitEnd = $derived(_end ?? raw(end));
191
+
192
+ const opts = $derived(formatOptions ?? DEFAULT_DATE_FORMAT);
193
+ const fmt = (iso: IsoDate): string => formatIsoDate(iso, locale, opts);
194
+
195
+ let display = $derived.by(() => {
196
+ if (_start && _end) {
197
+ return format
198
+ ? format(_start, _end)
199
+ : formatIsoDateRange(_start, _end, locale, opts);
200
+ }
201
+ if (_start) return `${fmt(_start)} – …`;
202
+ if (_end) return `… – ${fmt(_end)}`;
203
+ return "";
204
+ });
205
+
206
+ function commit(s: IsoDate | null, e: IsoDate | null) {
207
+ start = s;
208
+ end = e;
209
+ onChange?.({ start: s, end: e });
210
+ // let the hidden inputs flush before the validate action reads them
211
+ tick().then(() => shell?.dispatchChange());
212
+ }
213
+
214
+ function onRangeChange(r: CalendarRangeChange) {
215
+ commit(r.start, r.end);
216
+ if (!embedded && closeOnSelect && r.complete) shell?.close();
217
+ }
218
+
219
+ // Built-in guard (hidden inputs never validate natively): required, a complete
220
+ // range, valid ISO dates, min / max, disabled dates. Consumer opts are honored:
221
+ // `false` disables; an object is merged (its customValidator runs after the guard).
222
+ const validateOpts = $derived.by<
223
+ boolean | Omit<ValidateOptions, "setValidationResult">
224
+ >(() => {
225
+ if (validateProp === false) return false;
226
+ const custom = validateProp && typeof validateProp === "object" ? validateProp : {};
227
+ return {
228
+ ...custom,
229
+ // `val` is the start hidden input's value; the end is read from state.
230
+ customValidator: (val, ctx, el) => {
231
+ const s = `${val ?? ""}`.trim();
232
+ const e = submitEnd.trim();
233
+ if (!s && !e) {
234
+ if (required) return t("field_req_att");
235
+ return custom.customValidator?.(val, ctx, el) || "";
236
+ }
237
+ if (!s || !e) return t("range_incomplete");
238
+ const sIso = normalizeIsoDate(s);
239
+ const eIso = normalizeIsoDate(e);
240
+ if (!sIso || !eIso) return t("date_invalid");
241
+ if (compareIso(sIso, eIso) > 0) return t("range_incomplete");
242
+ for (const iso of [sIso, eIso]) {
243
+ if (_min && compareIso(iso, _min) < 0) {
244
+ return t("date_before_min", { value: fmt(_min) });
245
+ }
246
+ if (_max && compareIso(iso, _max) > 0) {
247
+ return t("date_after_max", { value: fmt(_max) });
248
+ }
249
+ if (isDateDisabled?.(iso, dayCellOf(iso, { zone, weekendDays }))) {
250
+ return t("date_disabled");
251
+ }
252
+ }
253
+ return custom.customValidator?.(val, ctx, el) || "";
254
+ },
255
+ };
256
+ });
257
+
258
+ // ---- Imperative API (satisfies ValidatableField), forwarded to the shell ----
259
+
260
+ /** Trigger validation now. Renders the inline message if invalid. */
261
+ export function validate(): ValidationResult | undefined {
262
+ return shell?.validate();
263
+ }
264
+
265
+ /** Clear the inline validation message and reset `setCustomValidity`. */
266
+ export function clearValidation(): void {
267
+ shell?.clearValidation();
268
+ }
269
+
270
+ /** Current validation state, or undefined if the validator has never run. */
271
+ export function getValidation(): ValidationResult | undefined {
272
+ return shell?.getValidation();
273
+ }
274
+
275
+ /** Focus the trigger (or the embedded calendar's tabbable day). */
276
+ export function focus(): void {
277
+ shell?.focus();
278
+ }
279
+
280
+ /** Scroll the field into view. Defaults to smooth + center. */
281
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
282
+ shell?.scrollIntoView(opts);
283
+ }
284
+
285
+ /** Open the picker dialog (no-op when `embedded`). */
286
+ export function open(): void {
287
+ shell?.open();
288
+ }
289
+
290
+ /** Close the picker dialog. */
291
+ export function close(): void {
292
+ shell?.close();
293
+ }
294
+ </script>
295
+
296
+ {#snippet doneFooter()}
297
+ <Button type="button" size="sm" intent="primary" onclick={() => shell?.close()}>
298
+ {t("done")}
299
+ </Button>
300
+ {/snippet}
301
+
302
+ <FieldDateShell
303
+ bind:this={shell}
304
+ bind:input
305
+ {id}
306
+ {label}
307
+ {description}
308
+ {labelAfter}
309
+ {below}
310
+ class={classProp}
311
+ {style}
312
+ {renderSize}
313
+ {required}
314
+ {disabled}
315
+ {tabindex}
316
+ {labelLeft}
317
+ {labelLeftWidth}
318
+ {labelLeftBreakpoint}
319
+ {classLabel}
320
+ {classLabelBox}
321
+ {classInputBox}
322
+ {classInputBoxWrap}
323
+ {classInputBoxWrapInvalid}
324
+ {classDescBox}
325
+ {classDescBoxToggle}
326
+ {classBelowBox}
327
+ {classValidationBox}
328
+ {display}
329
+ placeholder={placeholder ?? t("placeholder_range")}
330
+ {clearable}
331
+ clearLabel={t("clear_value")}
332
+ {embedded}
333
+ inputs={[
334
+ { name: nameStart, value: submitStart },
335
+ { name: nameEnd, value: submitEnd },
336
+ ]}
337
+ validate={validateOpts}
338
+ dialogTitle={t("dialog_title_range")}
339
+ closeLabel={t("close")}
340
+ {noScrollLock}
341
+ {classInput}
342
+ {classDialog}
343
+ onClear={() => commit(null, null)}
344
+ >
345
+ <Calendar
346
+ mode="range"
347
+ start={_start}
348
+ end={_end}
349
+ months={effectiveMonths}
350
+ {min}
351
+ {max}
352
+ {isDateDisabled}
353
+ {weekStartsOn}
354
+ {weekendDays}
355
+ {zone}
356
+ {locale}
357
+ {captionLayout}
358
+ {yearRange}
359
+ {showWeekNumbers}
360
+ {showOutsideDays}
361
+ {fixedWeeks}
362
+ {showToday}
363
+ showClear={embedded && clearable}
364
+ {renderDay}
365
+ {disabled}
366
+ {t}
367
+ focusOnMount={!embedded}
368
+ aria-labelledby={embedded && label ? `${id}-label` : undefined}
369
+ footer={!embedded && !closeOnSelect ? doneFooter : undefined}
370
+ {onRangeChange}
371
+ {...calendarProps}
372
+ />
373
+ </FieldDateShell>
@@ -0,0 +1,90 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import type { TranslateFn } from "../../types.js";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { Props as CalendarProps } from "../Calendar/Calendar.svelte";
6
+ import type { IsoDate } from "../Calendar/iso-date.js";
7
+ import type { FieldDateCalendarProps } from "./FieldDate.svelte";
8
+ import type { InputWrapClassProps } from "./types.js";
9
+ type SnippetWithId = Snippet<[{
10
+ id: string;
11
+ }]>;
12
+ /** Payload of `onChange`. */
13
+ export interface FieldDateRangeValue {
14
+ start: IsoDate | null;
15
+ end: IsoDate | null;
16
+ }
17
+ export interface Props extends InputWrapClassProps, FieldDateCalendarProps {
18
+ /** Range start as `YYYY-MM-DD` (bindable). */
19
+ start?: IsoDate | null;
20
+ /** Range end as `YYYY-MM-DD` (bindable). */
21
+ end?: IsoDate | null;
22
+ /** Form field name of the hidden input carrying `start`. */
23
+ nameStart?: string;
24
+ /** Form field name of the hidden input carrying `end`. */
25
+ nameEnd?: string;
26
+ /** Render the calendar inline inside the field instead of a trigger + dialog. */
27
+ embedded?: boolean;
28
+ /** Close the dialog once the range is complete. Default `true`. */
29
+ closeOnSelect?: boolean;
30
+ /** Trigger text while empty. Defaults to `t("placeholder_range")`. */
31
+ placeholder?: string;
32
+ /** Offer clearing (trailing × on the trigger / "Clear" in the embedded footer). Default `true`. */
33
+ clearable?: boolean;
34
+ /** BCP 47 locale of the display text and the calendar names. Browser default when omitted. */
35
+ locale?: string;
36
+ /** `Intl.DateTimeFormat` options of the display text. Default `{ dateStyle: "medium" }`. */
37
+ formatOptions?: Intl.DateTimeFormatOptions;
38
+ /** Custom display formatter for a complete range (wins over `formatOptions`). */
39
+ format?: (start: IsoDate, end: IsoDate) => string;
40
+ /**
41
+ * Months the calendar shows side by side at the `md` breakpoint and up (below
42
+ * it always a single month). Default `2`.
43
+ */
44
+ months?: number;
45
+ /** Anything else for the inner `Calendar` (class hooks, `view`, …). Applied last. */
46
+ calendarProps?: Partial<CalendarProps>;
47
+ /** The first (start) hidden input (bindable). */
48
+ input?: HTMLInputElement;
49
+ label?: SnippetWithId | THC;
50
+ description?: SnippetWithId | THC;
51
+ labelAfter?: SnippetWithId | THC;
52
+ below?: SnippetWithId | THC;
53
+ class?: string;
54
+ id?: string;
55
+ tabindex?: number;
56
+ renderSize?: "sm" | "md" | "lg" | string;
57
+ required?: boolean;
58
+ disabled?: boolean;
59
+ /**
60
+ * Validation. `false` disables the built-in guard (required / complete range /
61
+ * valid ISO / min / max / disabled dates); an options object is merged with it
62
+ * (your `customValidator` runs after the built-in checks pass). Default: the guard.
63
+ */
64
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
65
+ labelLeft?: boolean;
66
+ labelLeftWidth?: "normal" | "wide";
67
+ labelLeftBreakpoint?: number;
68
+ /** Classes for the trigger button */
69
+ classInput?: string;
70
+ /** Classes for the dialog box */
71
+ classDialog?: string;
72
+ style?: string;
73
+ noScrollLock?: boolean;
74
+ /** i18n — see `createCalendarT`. */
75
+ t?: TranslateFn;
76
+ /** Called after either end changed through the picker or the × (not on external writes). */
77
+ onChange?: (range: FieldDateRangeValue) => void;
78
+ }
79
+ import type { ValidationResult } from "../../actions/validate.svelte.js";
80
+ declare const FieldDateRange: import("svelte").Component<Props, {
81
+ validate: () => ValidationResult | undefined;
82
+ clearValidation: () => void;
83
+ getValidation: () => ValidationResult | undefined;
84
+ focus: () => void;
85
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
86
+ open: () => void;
87
+ close: () => void;
88
+ }, "input" | "end" | "start">;
89
+ type FieldDateRange = ReturnType<typeof FieldDateRange>;
90
+ export default FieldDateRange;
@@ -4,22 +4,24 @@ A comprehensive form input system with multiple field components, validation sup
4
4
 
5
5
  ## Components
6
6
 
7
- | Component | Description |
8
- | ----------------- | ---------------------------------------------------- |
9
- | `FieldInput` | Text, email, password, number, and other input types |
10
- | `FieldMoney` | Money amount stored as integer minor units (cents) |
11
- | `FieldTextarea` | Multi-line text input with auto-grow |
12
- | `FieldSelect` | Dropdown select with option groups |
13
- | `FieldCheckbox` | Single checkbox with label |
14
- | `FieldRadios` | Radio button group |
15
- | `FieldSwitch` | Toggle switch field |
16
- | `FieldFile` | File upload input |
17
- | `FieldAssets` | Asset/image upload with preview |
18
- | `FieldKeyValues` | Key-value pairs editor with JSON serialization |
19
- | `FieldLikeButton` | Like/favorite toggle button |
20
- | `Fieldset` | Fieldset with legend |
21
- | `Honeypot` | Hidden anti-bot trap field (server-less) |
22
- | `TimeTrap` | Anti-bot submit-timing primitive (server-less) |
7
+ | Component | Description |
8
+ | ----------------- | -------------------------------------------------------------- |
9
+ | `FieldInput` | Text, email, password, number, and other input types |
10
+ | `FieldMoney` | Money amount stored as integer minor units (cents) |
11
+ | `FieldDate` | Single calendar date — trigger + dialog, or embedded calendar |
12
+ | `FieldDateRange` | Inclusive date range (`start` … `end`), same two presentations |
13
+ | `FieldTextarea` | Multi-line text input with auto-grow |
14
+ | `FieldSelect` | Dropdown select with option groups |
15
+ | `FieldCheckbox` | Single checkbox with label |
16
+ | `FieldRadios` | Radio button group |
17
+ | `FieldSwitch` | Toggle switch field |
18
+ | `FieldFile` | File upload input |
19
+ | `FieldAssets` | Asset/image upload with preview |
20
+ | `FieldKeyValues` | Key-value pairs editor with JSON serialization |
21
+ | `FieldLikeButton` | Like/favorite toggle button |
22
+ | `Fieldset` | Fieldset with legend |
23
+ | `Honeypot` | Hidden anti-bot trap field (server-less) |
24
+ | `TimeTrap` | Anti-bot submit-timing primitive (server-less) |
23
25
 
24
26
  ## Common Props (FieldInput, FieldTextarea, FieldSelect)
25
27
 
@@ -734,6 +736,137 @@ until it settles, and a rejection is caught so a failed download never breaks th
734
736
 
735
737
  ---
736
738
 
739
+ ## Date and date range fields
740
+
741
+ `FieldDate` picks one calendar date, `FieldDateRange` an inclusive `start` … `end` pair.
742
+ Both wrap the [`Calendar`](../Calendar/README.md) component in the standard field
743
+ scaffolding (label, description, validation, sizes, the shared class props, the
744
+ imperative API) and come in two presentations:
745
+
746
+ - **dialog** (default) — a trigger button shows the formatted value (or a placeholder)
747
+ and opens the calendar in a native `<dialog>`; a pick closes it (`closeOnSelect`), a
748
+ trailing × clears. Focus moves into the grid on open and back to the trigger on close.
749
+ On phones the dialog is a centered card with 44px day targets.
750
+ - **embedded** — the calendar sits inline inside the field card, no trigger, no dialog;
751
+ "Clear" moves into the calendar's footer.
752
+
753
+ Values are ISO `YYYY-MM-DD` strings (`null` = empty). The `name` (`nameStart` /
754
+ `nameEnd` for the range) goes on **hidden inputs** carrying those ISO values, so a
755
+ `<form>` always submits `2026-09-02` no matter how the date is displayed — `locale`,
756
+ `formatOptions` and `format` only shape the trigger text.
757
+
758
+ ```svelte
759
+ <script lang="ts">
760
+ import { FieldDate, FieldDateRange, todayIso } from "@marianmeres/stuic";
761
+
762
+ let delivery = $state<string | null>(null);
763
+ let from = $state<string | null>(null);
764
+ let to = $state<string | null>(null);
765
+ let dob = $state<string | null>(null);
766
+ </script>
767
+
768
+ <FieldDate
769
+ bind:value={delivery}
770
+ label="Delivery date"
771
+ name="delivery"
772
+ min={todayIso()}
773
+ required
774
+ />
775
+
776
+ <FieldDateRange
777
+ bind:start={from}
778
+ bind:end={to}
779
+ label="Stay"
780
+ nameStart="from"
781
+ nameEnd="to"
782
+ />
783
+
784
+ <!-- inline, with a dropdown caption to reach far-away years -->
785
+ <FieldDate
786
+ bind:value={dob}
787
+ label="Date of birth"
788
+ name="dob"
789
+ embedded
790
+ captionLayout="dropdown"
791
+ yearRange={[1920, 2026]}
792
+ max={todayIso()}
793
+ />
794
+ ```
795
+
796
+ ### Props
797
+
798
+ Field-specific:
799
+
800
+ | Prop | `FieldDate` | `FieldDateRange` | Description |
801
+ | --------------- | ------------------ | --------------------------------------- | ------------------------------------------------------------ |
802
+ | value | `value` (bindable) | `start`, `end` (bindable) | ISO `YYYY-MM-DD` or `null` |
803
+ | form name | `name` | `nameStart`, `nameEnd` | Names of the hidden input(s) |
804
+ | `onChange` | `(value) => void` | `({ start, end }) => void` | After a pick / clear through the UI (not on external writes) |
805
+ | `format` | `(iso) => string` | `(start, end) => string` | Custom trigger text (wins over `formatOptions`) |
806
+ | `months` | default `1` | default `2` (collapses to 1 below `md`) | Months side by side in the calendar |
807
+ | `closeOnSelect` | close after a pick | close once the range is complete | Default `true`; `false` keeps the dialog open + adds "Done" |
808
+
809
+ Shared:
810
+
811
+ | Prop | Type | Default | Description |
812
+ | --------------- | ---------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
813
+ | `embedded` | `boolean` | `false` | Inline calendar instead of trigger + dialog |
814
+ | `placeholder` | `string` | `t("placeholder_…")` | Trigger text while empty |
815
+ | `clearable` | `boolean` | `true` | The trailing × (dialog) / the footer "Clear" (embedded) |
816
+ | `locale` | `string` | browser | BCP 47 locale of the trigger text and the calendar's day / month names |
817
+ | `formatOptions` | `Intl.DateTimeFormatOptions` | `{ dateStyle: "medium" }` | Trigger text format |
818
+ | `calendarProps` | `Partial<CalendarProps>` | - | Anything else for the inner `Calendar` (class hooks, `view`, …), applied last |
819
+ | `validate` | `boolean \| ValidateOptions` | enabled | `false` disables the built-in guard; an object is merged with it (your validator runs after it) |
820
+ | `classInput` | `string` | - | Trigger button classes |
821
+ | `classDialog` | `string` | - | Dialog card classes |
822
+ | `noScrollLock` | `boolean` | `false` | Don't lock body scroll while the dialog is open |
823
+ | `t` | `TranslateFn` | English | i18n — see the [Calendar README](../Calendar/README.md#i18n) (Slovak bundled: `CALENDAR_MESSAGES_SK`) |
824
+ | `input` | `HTMLInputElement` | - | The (first) hidden input (bindable) |
825
+
826
+ Forwarded to the calendar unchanged (see its README): `min`, `max`, `isDateDisabled`,
827
+ `weekStartsOn`, `weekendDays`, `zone`, `captionLayout`, `yearRange`, `showWeekNumbers`,
828
+ `showOutsideDays`, `fixedWeeks`, `showToday`, `renderDay`. Plus the usual field props:
829
+ `label`, `description`, `labelAfter`, `below`, `id`, `tabindex`, `renderSize`,
830
+ `required`, `disabled`, `labelLeft*`, `style`, and the shared `InputWrapClassProps`.
831
+
832
+ ### Validation
833
+
834
+ A hidden input is barred from native constraint validation, so both fields enforce
835
+ everything in their own validator (default-on, like the other hidden-input fields):
836
+ `required`, a valid ISO date, `min` / `max`, `isDateDisabled` — and the range field
837
+ rejects a half range (a start without an end) **whether or not** it is `required`. The
838
+ messages go through `t` and format the bound with the field's own display format
839
+ ("Date must be on or after Feb 5, 2026."). A consumer `customValidator` runs after the
840
+ guard passes.
841
+
842
+ Loose input is tolerated on the way in: `"2026-09-02T10:00:00Z"` or a `Date` are reduced
843
+ to their calendar date (the date portion **as written**, never shifted through a zone).
844
+ Unparseable input is displayed as empty but submitted as-is, so the validator flags it
845
+ instead of silently posting an empty value.
846
+
847
+ ### Methods
848
+
849
+ Accessed via a `bind:this` reference — the imperative validate API (`validate()`,
850
+ `clearValidation()`, `getValidation()`, `focus()`, `scrollIntoView()`) plus `open()` /
851
+ `close()` for the dialog (no-ops when `embedded`).
852
+
853
+ ### CSS Variables
854
+
855
+ The calendar inside is styled by the `--stuic-calendar-*` tokens (see its README); the
856
+ field shell adds:
857
+
858
+ | Variable | Default | Description |
859
+ | ------------------------------------- | -------------------------------- | ------------------------------------ |
860
+ | `--stuic-field-date-icon-text` | `--stuic-color-muted-foreground` | Trigger calendar icon color |
861
+ | `--stuic-field-date-placeholder-text` | `--stuic-input-placeholder` | Trigger placeholder color |
862
+ | `--stuic-field-date-embedded-padding` | `0.5rem` | Padding around the embedded calendar |
863
+ | `--stuic-field-date-dialog-padding` | `1rem` | Dialog card padding |
864
+ | `--stuic-field-date-dialog-radius` | `--stuic-radius-container` | Dialog card radius |
865
+ | `--stuic-field-date-dialog-shadow` | `--stuic-shadow-dialog` | Dialog card shadow |
866
+
867
+ The trigger follows the `--stuic-input-*` size tokens (`renderSize`), the dialog card the
868
+ `--stuic-modal-dialog-bg` / `-text` tokens.
869
+
737
870
  ## Honeypot & TimeTrap (anti-bot primitives)
738
871
 
739
872
  Two small, **client-side, server-less** primitives for cheap spam mitigation. They produce **signals** — they do not block anything. Read the signal, then enforce on your server (the only place enforcement is trustworthy). Both are reusable on any form; [`ContactUsForm`](../ContactUsForm/README.md) composes them by default.