@marianmeres/stuic 3.126.1 → 3.128.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.
@@ -0,0 +1,571 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type {
6
+ ContactBotCheck,
7
+ ContactFieldConfig,
8
+ ContactFormData,
9
+ ContactFormValidationError,
10
+ } from "./_internal/contact-form-types.js";
11
+ import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
12
+
13
+ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
14
+ /** Bindable contact data. Default: createEmptyContactFormData() */
15
+ formData?: ContactFormData;
16
+
17
+ /**
18
+ * Called on submit after client-side validation passes.
19
+ * `botCheck` carries the anti-bot signals (report-only — enforce them
20
+ * server-side; the form never blocks a submit because of them).
21
+ */
22
+ onSubmit: (data: ContactFormData, botCheck: ContactBotCheck) => void;
23
+
24
+ /** Whether the form is currently submitting (disables CTA). */
25
+ isSubmitting?: boolean;
26
+
27
+ /** Field-specific validation errors (e.g. from the server). */
28
+ errors?: ContactFormValidationError[];
29
+
30
+ /**
31
+ * General error message (not field-specific). Rendered as an alert box
32
+ * above the form. Example: "Something went wrong, please try again."
33
+ */
34
+ error?: string;
35
+
36
+ /** Show the Name field. Default: false */
37
+ showName?: boolean;
38
+ /** Require the Name field (only applies when shown). Default: true */
39
+ requireName?: boolean;
40
+ /** Show the Phone field. Default: false */
41
+ showPhone?: boolean;
42
+ /** Require the Phone field (only applies when shown). Default: false */
43
+ requirePhone?: boolean;
44
+ /** Show the Subject field. Default: false */
45
+ showSubject?: boolean;
46
+ /** Require the Subject field (only applies when shown). Default: false */
47
+ requireSubject?: boolean;
48
+ /**
49
+ * When provided (non-empty), the Subject renders as a `<select>` of these
50
+ * values instead of a free-text input, and the subject field is shown
51
+ * regardless of `showSubject`. A blank "select…" prompt is prepended so the
52
+ * initial empty value isn't silently auto-selected. The bound value is still
53
+ * the chosen string in `formData.subject`.
54
+ */
55
+ subjectValues?: string[];
56
+ /** Show the Company field. Default: false */
57
+ showCompany?: boolean;
58
+ /** Require the Company field (only applies when shown). Default: false */
59
+ requireCompany?: boolean;
60
+ /** Minimum message length. 0 (default) disables the check. */
61
+ messageMinLength?: number;
62
+
63
+ /**
64
+ * Declarative extra fields rendered as FieldInput entries.
65
+ * Values bind into `formData.extra[name]`.
66
+ */
67
+ extraFields?: ContactFieldConfig[];
68
+
69
+ /**
70
+ * Escape hatch for non-FieldInput extras (consent checkbox, custom widgets).
71
+ * Rendered after the declarative extras and before the submit button.
72
+ * Receives `formData` (bindable) and a `fieldError(name)` lookup.
73
+ */
74
+ extraFieldsSlot?: Snippet<
75
+ [
76
+ {
77
+ formData: ContactFormData;
78
+ fieldError: (name: string) => string | undefined;
79
+ },
80
+ ]
81
+ >;
82
+
83
+ /** Render the hidden honeypot trap. Default: true */
84
+ useHoneypot?: boolean;
85
+ /**
86
+ * Honeypot field name — tempting to bots but NOT a browser-autofill token
87
+ * (avoid url/website/email/name/phone/address). Default: "link".
88
+ */
89
+ honeypotName?: string;
90
+ /** Render the time-trap (rejects suspiciously fast submits). Default: true */
91
+ useTimeTrap?: boolean;
92
+ /** Minimum expected fill time (ms) before a submit looks human. Default: 2000 */
93
+ timeTrapMinMs?: number;
94
+
95
+ /** Override CTA label. */
96
+ submitLabel?: string;
97
+ /** Override CTA label while submitting. */
98
+ submittingLabel?: string;
99
+ /** Override the CTA section. */
100
+ submitButton?: Snippet<[{ isSubmitting: boolean; disabled: boolean }]>;
101
+
102
+ /** Content below the form (legal text, alternative contact links, etc.). */
103
+ footer?: Snippet;
104
+
105
+ /** Optional notifications instance — `error` is also sent via notifications.error(). */
106
+ notifications?: NotificationsStack;
107
+
108
+ t?: TranslateFn;
109
+ unstyled?: boolean;
110
+ class?: string;
111
+ el?: HTMLFormElement;
112
+ }
113
+ </script>
114
+
115
+ <script lang="ts">
116
+ import { untrack } from "svelte";
117
+ import { twMerge } from "../../utils/tw-merge.js";
118
+ import { t_default } from "./_internal/contact-form-i18n-defaults.js";
119
+ import {
120
+ createEmptyContactFormData,
121
+ validateContactForm,
122
+ } from "./_internal/contact-form-utils.js";
123
+ import Button from "../Button/Button.svelte";
124
+ import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
125
+ import FieldInput from "../Input/FieldInput.svelte";
126
+ import FieldSelect from "../Input/FieldSelect.svelte";
127
+ import FieldTextarea from "../Input/FieldTextarea.svelte";
128
+ import Honeypot from "../Input/Honeypot.svelte";
129
+ import TimeTrap from "../Input/TimeTrap.svelte";
130
+ import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
131
+ import {
132
+ scrollToFirstInvalidField,
133
+ validateAllFields,
134
+ } from "../../utils/validate-fields.js";
135
+
136
+ let {
137
+ formData = $bindable(createEmptyContactFormData()),
138
+ onSubmit,
139
+ isSubmitting = false,
140
+ errors: externalErrors = [],
141
+ error,
142
+ showName = false,
143
+ requireName = true,
144
+ showPhone = false,
145
+ requirePhone = false,
146
+ showSubject = false,
147
+ requireSubject = false,
148
+ subjectValues,
149
+ showCompany = false,
150
+ requireCompany = false,
151
+ messageMinLength = 0,
152
+ extraFields = [],
153
+ extraFieldsSlot,
154
+ useHoneypot = true,
155
+ honeypotName = "link",
156
+ useTimeTrap = true,
157
+ timeTrapMinMs = 2000,
158
+ submitLabel,
159
+ submittingLabel,
160
+ submitButton,
161
+ footer,
162
+ notifications,
163
+ t: tProp,
164
+ unstyled = false,
165
+ class: classProp,
166
+ el = $bindable(),
167
+ ...rest
168
+ }: Props = $props();
169
+
170
+ let t = $derived(tProp ?? t_default);
171
+
172
+ // Mirror the form ref into local $state so it survives prop re-application
173
+ // when the parent re-renders without binding `el`. Same Svelte 5 `$bindable`
174
+ // + `bind:this` gotcha that LoginForm / RegisterForm document.
175
+ let formEl = $state<HTMLFormElement | undefined>();
176
+ $effect(() => {
177
+ el = formEl;
178
+ });
179
+
180
+ let topFields = $derived(extraFields.filter((f) => f.position === "top"));
181
+ let bottomFields = $derived(extraFields.filter((f) => f.position !== "top"));
182
+
183
+ // Subject: render a <select> when subjectValues is non-empty (which also shows
184
+ // the field regardless of showSubject); otherwise a free-text input gated by
185
+ // showSubject. The select gets a prepended blank "prompt" option so the initial
186
+ // empty subject isn't silently auto-selected to the first real value.
187
+ let subjectAsSelect = $derived((subjectValues?.length ?? 0) > 0);
188
+ let subjectShown = $derived(showSubject || subjectAsSelect);
189
+ let subjectOptions = $derived([
190
+ { label: t("contact_form.subject_select_prompt"), value: "" },
191
+ ...(subjectValues ?? []).map((v) => ({ label: v, value: v })),
192
+ ]);
193
+
194
+ // Bot-protection state (kept out of formData — surfaced via botCheck).
195
+ let honeypotValue = $state("");
196
+ let timeTrap = $state<TimeTrap>();
197
+ let ttTooFast = $state(true);
198
+ let ttElapsed = $state(0);
199
+
200
+ // Internal validation errors (set on submit).
201
+ let internalErrors = $state<ContactFormValidationError[]>([]);
202
+
203
+ // Clear internal field errors as soon as the user edits any tracked field, so
204
+ // a previous failed submit's errors don't linger after the user has fixed
205
+ // them. `untrack` the read+write so this effect re-runs only on formData
206
+ // changes (not when handleSubmitValid sets internalErrors).
207
+ $effect(() => {
208
+ void formData.name;
209
+ void formData.email;
210
+ void formData.phone;
211
+ void formData.subject;
212
+ void formData.company;
213
+ void formData.message;
214
+ for (const f of extraFields) void formData.extra?.[f.name];
215
+ untrack(() => {
216
+ if (internalErrors.length) internalErrors = [];
217
+ });
218
+ });
219
+
220
+ // Merge internal + external errors; external takes precedence per field.
221
+ let allErrors = $derived.by(() => {
222
+ const map = new Map<string, string>();
223
+ for (const e of internalErrors) map.set(e.field, e.message);
224
+ for (const e of externalErrors) map.set(e.field, e.message);
225
+ return [...map.entries()].map(([field, message]) => ({ field, message }));
226
+ });
227
+
228
+ function fieldError(field: string): string | undefined {
229
+ return allErrors.find((e) => e.field === field)?.message;
230
+ }
231
+
232
+ function extraValue(cfg: ContactFieldConfig): string {
233
+ const v = formData.extra?.[cfg.name];
234
+ if (v == null) return typeof cfg.initialValue === "string" ? cfg.initialValue : "";
235
+ return typeof v === "string" ? v : String(v);
236
+ }
237
+
238
+ function setExtraValue(cfg: ContactFieldConfig, value: string) {
239
+ if (!formData.extra) formData.extra = {};
240
+ formData.extra[cfg.name] = value;
241
+ }
242
+
243
+ function buildBotCheck(): ContactBotCheck {
244
+ const snap = useTimeTrap
245
+ ? (timeTrap?.check() ?? { elapsedMs: ttElapsed, isTooFast: ttTooFast })
246
+ : { elapsedMs: 0, isTooFast: false };
247
+ const honeypot = useHoneypot ? honeypotValue : "";
248
+ const honeypotFilled = useHoneypot && honeypot.trim() !== "";
249
+ const isTooFast = useTimeTrap && !!snap.isTooFast;
250
+ return {
251
+ honeypot,
252
+ honeypotFilled,
253
+ elapsedMs: snap.elapsedMs ?? 0,
254
+ isTooFast,
255
+ minMs: timeTrapMinMs,
256
+ isLikelyBot: honeypotFilled || isTooFast,
257
+ };
258
+ }
259
+
260
+ function handleSubmitValid() {
261
+ const validationErrors = validateContactForm(formData, t, {
262
+ showName,
263
+ requireName,
264
+ showPhone,
265
+ requirePhone,
266
+ showSubject: subjectShown,
267
+ requireSubject,
268
+ showCompany,
269
+ requireCompany,
270
+ messageMinLength,
271
+ extraFields,
272
+ });
273
+ internalErrors = validationErrors;
274
+
275
+ // Report-only on bot signals: we still submit when field validation passes
276
+ // and hand the consumer the botCheck to enforce server-side.
277
+ if (validationErrors.length === 0 && externalErrors.length === 0) {
278
+ onSubmit(formData, buildBotCheck());
279
+ }
280
+ }
281
+
282
+ $effect(() => {
283
+ if (error && notifications) notifications.error(error);
284
+ });
285
+
286
+ // onSubmitValidityCheck intercepts native submit and dispatches "submit_valid".
287
+ // Bind to `formEl` (local state) — NOT the `el` prop, which can revert to
288
+ // undefined on parent re-render and would silently detach the listener.
289
+ $effect(() => {
290
+ const node = formEl;
291
+ if (!node) return;
292
+ node.addEventListener("submit_valid", handleSubmitValid);
293
+ return () => node.removeEventListener("submit_valid", handleSubmitValid);
294
+ });
295
+
296
+ let _class = $derived(
297
+ unstyled ? classProp : twMerge("stuic-contact-us-form", classProp)
298
+ );
299
+
300
+ // Imperative API ----------------------------------------------------------
301
+ let topFieldRefs: (FieldInput | undefined)[] = $state([]);
302
+ let bottomFieldRefs: (FieldInput | undefined)[] = $state([]);
303
+ let nameField = $state<FieldInput>();
304
+ let emailField = $state<FieldInput>();
305
+ let phoneField = $state<FieldInput>();
306
+ let companyField = $state<FieldInput>();
307
+ let subjectField = $state<FieldInput | FieldSelect>();
308
+ let messageField = $state<FieldTextarea>();
309
+
310
+ function _fields() {
311
+ return [
312
+ ...topFieldRefs,
313
+ ...(showName ? [nameField] : []),
314
+ emailField,
315
+ ...(showPhone ? [phoneField] : []),
316
+ ...(showCompany ? [companyField] : []),
317
+ ...(subjectShown ? [subjectField] : []),
318
+ messageField,
319
+ ...bottomFieldRefs,
320
+ ];
321
+ }
322
+
323
+ /**
324
+ * Run every visible field's validator and render any inline errors. Returns
325
+ * true if all fields are valid. Useful from custom submit handlers.
326
+ */
327
+ export function validate(): boolean {
328
+ return validateAllFields(_fields());
329
+ }
330
+
331
+ /**
332
+ * Scroll the first invalid field into view and focus it. Returns true if a
333
+ * field was scrolled. Call after `validate()`.
334
+ */
335
+ export function scrollToFirstError(
336
+ opts?: Parameters<typeof scrollToFirstInvalidField>[1]
337
+ ): boolean {
338
+ return scrollToFirstInvalidField(_fields(), opts);
339
+ }
340
+ </script>
341
+
342
+ <form bind:this={formEl} class={_class} use:onSubmitValidityCheck novalidate {...rest}>
343
+ <!-- General error alert -->
344
+ <DismissibleMessage message={error} intent="destructive" />
345
+
346
+ <!-- Top-position extra fields -->
347
+ {#each topFields as cfg, i (cfg.name)}
348
+ <FieldInput
349
+ bind:this={topFieldRefs[i]}
350
+ value={extraValue(cfg)}
351
+ oninput={(e: Event) =>
352
+ setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
353
+ label={cfg.label}
354
+ type={cfg.type ?? "text"}
355
+ placeholder={cfg.placeholder}
356
+ autocomplete={cfg.autocomplete}
357
+ required={cfg.required}
358
+ name={`contact-extra-${cfg.name}`}
359
+ labelLeftBreakpoint={0}
360
+ validate={{
361
+ customValidator() {
362
+ return fieldError(cfg.name) || "";
363
+ },
364
+ }}
365
+ {...cfg.props}
366
+ />
367
+ {/each}
368
+
369
+ <!--
370
+ NOTE on `binding_property_non_reactive`: formData is a $bindable prop — deep
371
+ reactivity depends on the consumer passing a $state() object. The bindings
372
+ work correctly regardless; the per-field directives below silence the hint.
373
+ (This block intentionally does NOT start with the literal directive word so
374
+ it isn't parsed as one.)
375
+ -->
376
+ <!-- Name -->
377
+ {#if showName}
378
+ <!-- svelte-ignore binding_property_non_reactive -->
379
+ <FieldInput
380
+ bind:this={nameField}
381
+ bind:value={formData.name}
382
+ label={t("contact_form.name_label")}
383
+ type="text"
384
+ placeholder={t("contact_form.name_placeholder")}
385
+ autocomplete="name"
386
+ required={requireName}
387
+ name="contact-name"
388
+ labelLeftBreakpoint={0}
389
+ validate={{
390
+ customValidator() {
391
+ return fieldError("name") || "";
392
+ },
393
+ }}
394
+ />
395
+ {/if}
396
+
397
+ <!-- Email -->
398
+ <!-- svelte-ignore binding_property_non_reactive -->
399
+ <FieldInput
400
+ bind:this={emailField}
401
+ bind:value={formData.email}
402
+ label={t("contact_form.email_label")}
403
+ type="email"
404
+ placeholder={t("contact_form.email_placeholder")}
405
+ autocomplete="email"
406
+ required
407
+ name="contact-email"
408
+ labelLeftBreakpoint={0}
409
+ validate={{
410
+ customValidator() {
411
+ return fieldError("email") || "";
412
+ },
413
+ }}
414
+ />
415
+
416
+ <!-- Phone -->
417
+ {#if showPhone}
418
+ <!-- svelte-ignore binding_property_non_reactive -->
419
+ <FieldInput
420
+ bind:this={phoneField}
421
+ bind:value={formData.phone}
422
+ label={t("contact_form.phone_label")}
423
+ type="tel"
424
+ placeholder={t("contact_form.phone_placeholder")}
425
+ autocomplete="tel"
426
+ required={requirePhone}
427
+ name="contact-phone"
428
+ labelLeftBreakpoint={0}
429
+ validate={{
430
+ customValidator() {
431
+ return fieldError("phone") || "";
432
+ },
433
+ }}
434
+ />
435
+ {/if}
436
+
437
+ <!-- Company -->
438
+ {#if showCompany}
439
+ <!-- svelte-ignore binding_property_non_reactive -->
440
+ <FieldInput
441
+ bind:this={companyField}
442
+ bind:value={formData.company}
443
+ label={t("contact_form.company_label")}
444
+ type="text"
445
+ placeholder={t("contact_form.company_placeholder")}
446
+ autocomplete="organization"
447
+ required={requireCompany}
448
+ name="contact-company"
449
+ labelLeftBreakpoint={0}
450
+ validate={{
451
+ customValidator() {
452
+ return fieldError("company") || "";
453
+ },
454
+ }}
455
+ />
456
+ {/if}
457
+
458
+ <!-- Subject — a <select> when subjectValues is provided, else free text -->
459
+ {#if subjectShown}
460
+ {#if subjectAsSelect}
461
+ <!-- svelte-ignore binding_property_non_reactive -->
462
+ <FieldSelect
463
+ bind:this={subjectField}
464
+ bind:value={formData.subject}
465
+ label={t("contact_form.subject_label")}
466
+ options={subjectOptions}
467
+ required={requireSubject}
468
+ name="contact-subject"
469
+ labelLeftBreakpoint={0}
470
+ validate={{
471
+ customValidator() {
472
+ return fieldError("subject") || "";
473
+ },
474
+ }}
475
+ />
476
+ {:else}
477
+ <!-- svelte-ignore binding_property_non_reactive -->
478
+ <FieldInput
479
+ bind:this={subjectField}
480
+ bind:value={formData.subject}
481
+ label={t("contact_form.subject_label")}
482
+ type="text"
483
+ placeholder={t("contact_form.subject_placeholder")}
484
+ required={requireSubject}
485
+ name="contact-subject"
486
+ labelLeftBreakpoint={0}
487
+ validate={{
488
+ customValidator() {
489
+ return fieldError("subject") || "";
490
+ },
491
+ }}
492
+ />
493
+ {/if}
494
+ {/if}
495
+
496
+ <!-- Message -->
497
+ <!-- svelte-ignore binding_property_non_reactive -->
498
+ <FieldTextarea
499
+ bind:this={messageField}
500
+ bind:value={formData.message}
501
+ label={t("contact_form.message_label")}
502
+ placeholder={t("contact_form.message_placeholder")}
503
+ required
504
+ name="contact-message"
505
+ labelLeftBreakpoint={0}
506
+ validate={{
507
+ customValidator() {
508
+ return fieldError("message") || "";
509
+ },
510
+ }}
511
+ />
512
+
513
+ <!-- Bottom-position extra fields (default) -->
514
+ {#each bottomFields as cfg, i (cfg.name)}
515
+ <FieldInput
516
+ bind:this={bottomFieldRefs[i]}
517
+ value={extraValue(cfg)}
518
+ oninput={(e: Event) =>
519
+ setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
520
+ label={cfg.label}
521
+ type={cfg.type ?? "text"}
522
+ placeholder={cfg.placeholder}
523
+ autocomplete={cfg.autocomplete}
524
+ required={cfg.required}
525
+ name={`contact-extra-${cfg.name}`}
526
+ labelLeftBreakpoint={0}
527
+ validate={{
528
+ customValidator() {
529
+ return fieldError(cfg.name) || "";
530
+ },
531
+ }}
532
+ {...cfg.props}
533
+ />
534
+ {/each}
535
+
536
+ <!-- Escape-hatch slot (consent checkbox, custom fields, etc.) -->
537
+ {#if extraFieldsSlot}
538
+ {@render extraFieldsSlot({ formData, fieldError })}
539
+ {/if}
540
+
541
+ <!-- Bot protection (rendered, but never blocks submit — see buildBotCheck) -->
542
+ {#if useHoneypot}
543
+ <Honeypot bind:value={honeypotValue} name={honeypotName} />
544
+ {/if}
545
+ {#if useTimeTrap}
546
+ <TimeTrap
547
+ bind:this={timeTrap}
548
+ bind:isTooFast={ttTooFast}
549
+ bind:elapsedMs={ttElapsed}
550
+ minMs={timeTrapMinMs}
551
+ />
552
+ {/if}
553
+
554
+ <!-- CTA -->
555
+ {#if submitButton}
556
+ {@render submitButton({ isSubmitting, disabled: isSubmitting })}
557
+ {:else}
558
+ <div class={unstyled ? undefined : "stuic-contact-us-form-submit"}>
559
+ <Button intent="primary" type="submit" disabled={isSubmitting} class="w-full">
560
+ {isSubmitting
561
+ ? (submittingLabel ?? t("contact_form.submitting"))
562
+ : (submitLabel ?? t("contact_form.submit"))}
563
+ </Button>
564
+ </div>
565
+ {/if}
566
+
567
+ <!-- Footer -->
568
+ {#if footer}
569
+ {@render footer()}
570
+ {/if}
571
+ </form>
@@ -0,0 +1,101 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { TranslateFn } from "../../types.js";
4
+ import type { ContactBotCheck, ContactFieldConfig, ContactFormData, ContactFormValidationError } from "./_internal/contact-form-types.js";
5
+ import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
6
+ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
7
+ /** Bindable contact data. Default: createEmptyContactFormData() */
8
+ formData?: ContactFormData;
9
+ /**
10
+ * Called on submit after client-side validation passes.
11
+ * `botCheck` carries the anti-bot signals (report-only — enforce them
12
+ * server-side; the form never blocks a submit because of them).
13
+ */
14
+ onSubmit: (data: ContactFormData, botCheck: ContactBotCheck) => void;
15
+ /** Whether the form is currently submitting (disables CTA). */
16
+ isSubmitting?: boolean;
17
+ /** Field-specific validation errors (e.g. from the server). */
18
+ errors?: ContactFormValidationError[];
19
+ /**
20
+ * General error message (not field-specific). Rendered as an alert box
21
+ * above the form. Example: "Something went wrong, please try again."
22
+ */
23
+ error?: string;
24
+ /** Show the Name field. Default: false */
25
+ showName?: boolean;
26
+ /** Require the Name field (only applies when shown). Default: true */
27
+ requireName?: boolean;
28
+ /** Show the Phone field. Default: false */
29
+ showPhone?: boolean;
30
+ /** Require the Phone field (only applies when shown). Default: false */
31
+ requirePhone?: boolean;
32
+ /** Show the Subject field. Default: false */
33
+ showSubject?: boolean;
34
+ /** Require the Subject field (only applies when shown). Default: false */
35
+ requireSubject?: boolean;
36
+ /**
37
+ * When provided (non-empty), the Subject renders as a `<select>` of these
38
+ * values instead of a free-text input, and the subject field is shown
39
+ * regardless of `showSubject`. A blank "select…" prompt is prepended so the
40
+ * initial empty value isn't silently auto-selected. The bound value is still
41
+ * the chosen string in `formData.subject`.
42
+ */
43
+ subjectValues?: string[];
44
+ /** Show the Company field. Default: false */
45
+ showCompany?: boolean;
46
+ /** Require the Company field (only applies when shown). Default: false */
47
+ requireCompany?: boolean;
48
+ /** Minimum message length. 0 (default) disables the check. */
49
+ messageMinLength?: number;
50
+ /**
51
+ * Declarative extra fields rendered as FieldInput entries.
52
+ * Values bind into `formData.extra[name]`.
53
+ */
54
+ extraFields?: ContactFieldConfig[];
55
+ /**
56
+ * Escape hatch for non-FieldInput extras (consent checkbox, custom widgets).
57
+ * Rendered after the declarative extras and before the submit button.
58
+ * Receives `formData` (bindable) and a `fieldError(name)` lookup.
59
+ */
60
+ extraFieldsSlot?: Snippet<[
61
+ {
62
+ formData: ContactFormData;
63
+ fieldError: (name: string) => string | undefined;
64
+ }
65
+ ]>;
66
+ /** Render the hidden honeypot trap. Default: true */
67
+ useHoneypot?: boolean;
68
+ /**
69
+ * Honeypot field name — tempting to bots but NOT a browser-autofill token
70
+ * (avoid url/website/email/name/phone/address). Default: "link".
71
+ */
72
+ honeypotName?: string;
73
+ /** Render the time-trap (rejects suspiciously fast submits). Default: true */
74
+ useTimeTrap?: boolean;
75
+ /** Minimum expected fill time (ms) before a submit looks human. Default: 2000 */
76
+ timeTrapMinMs?: number;
77
+ /** Override CTA label. */
78
+ submitLabel?: string;
79
+ /** Override CTA label while submitting. */
80
+ submittingLabel?: string;
81
+ /** Override the CTA section. */
82
+ submitButton?: Snippet<[{
83
+ isSubmitting: boolean;
84
+ disabled: boolean;
85
+ }]>;
86
+ /** Content below the form (legal text, alternative contact links, etc.). */
87
+ footer?: Snippet;
88
+ /** Optional notifications instance — `error` is also sent via notifications.error(). */
89
+ notifications?: NotificationsStack;
90
+ t?: TranslateFn;
91
+ unstyled?: boolean;
92
+ class?: string;
93
+ el?: HTMLFormElement;
94
+ }
95
+ import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
96
+ declare const ContactUsForm: import("svelte").Component<Props, {
97
+ validate: () => boolean;
98
+ scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
99
+ }, "el" | "formData">;
100
+ type ContactUsForm = ReturnType<typeof ContactUsForm>;
101
+ export default ContactUsForm;