feat(contact): full validation, CMS-translated strings, smaller UI
- contact-form-validation.ts: shared client+server validator returning
{key, params} so messages can be translated via the CMS bundle.
- ContactFormComponent: 3 honeypots, subject field, GDPR consent
checkbox, character counter, inline per-field errors after submit,
uses useTranslate() everywhere.
- /api/contact: origin allowlist, multiple honeypot check, client-age
gate, server-side re-validation, forwards X-Forwarded-For + UA to
CMS forms plugin with consent timestamp in payload.
- TextInput/Textarea: new size="sm" variant (rounded-sm, shadow-sm,
h-10, smaller padding/text). Default "md" unchanged.
- translations.ts: 37 new contact_* keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,18 +11,25 @@
|
||||
error = '',
|
||||
helpText = '',
|
||||
invalid = false,
|
||||
size = 'md' as 'sm' | 'md',
|
||||
} = $props();
|
||||
|
||||
const inputId = $derived(id || `input-${Math.random().toString(36).slice(2)}`);
|
||||
const errorId = $derived(`${inputId}-error`);
|
||||
const helpId = $derived(`${inputId}-help`);
|
||||
|
||||
const sizeClasses = $derived(
|
||||
size === 'sm'
|
||||
? 'h-10 rounded-sm px-3 py-2 text-sm shadow-sm'
|
||||
: 'h-12 rounded-lg px-4 py-3 text-base'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label
|
||||
for={inputId}
|
||||
class="mb-1.5 block text-sm font-medium text-stein-700"
|
||||
class="mb-1.5 block {size === 'sm' ? 'text-xs' : 'text-sm'} font-medium text-stein-700"
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
@@ -41,16 +48,16 @@
|
||||
aria-required={required}
|
||||
aria-invalid={invalid || !!error}
|
||||
aria-describedby={[error ? errorId : '', helpText ? helpId : ''].filter(Boolean).join(' ')}
|
||||
class="h-12 w-full rounded-lg border bg-white px-4 py-3 text-base text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
class="{sizeClasses} w-full border bg-white text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
? 'border-error ring-2 ring-error-subtle'
|
||||
: 'border-stein-300 hover:border-stein-400 focus:border-wald-500 focus:ring-2 focus:ring-wald-100'} disabled:bg-stein-100 disabled:opacity-60"
|
||||
/>
|
||||
{#if error}
|
||||
<p id={errorId} class="mt-1.5 text-[0.8125rem] text-error" role="alert">
|
||||
<p id={errorId} class="mt-1 text-[0.75rem] text-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
{:else if helpText}
|
||||
<p id={helpId} class="mt-1.5 text-[0.8125rem] text-stein-500">
|
||||
<p id={helpId} class="mt-1 text-[0.75rem] text-stein-500">
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -11,18 +11,25 @@
|
||||
helpText = '',
|
||||
invalid = false,
|
||||
rows = 4,
|
||||
size = 'md' as 'sm' | 'md',
|
||||
} = $props();
|
||||
|
||||
const inputId = $derived(id || `textarea-${Math.random().toString(36).slice(2)}`);
|
||||
const errorId = $derived(`${inputId}-error`);
|
||||
const helpId = $derived(`${inputId}-help`);
|
||||
|
||||
const sizeClasses = $derived(
|
||||
size === 'sm'
|
||||
? 'min-h-[80px] rounded-sm px-3 py-2 text-sm shadow-sm'
|
||||
: 'min-h-[120px] rounded-lg px-4 py-3 text-base'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label
|
||||
for={inputId}
|
||||
class="mb-1.5 block text-sm font-medium text-stein-700"
|
||||
class="mb-1.5 block {size === 'sm' ? 'text-xs' : 'text-sm'} font-medium text-stein-700"
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
@@ -41,16 +48,16 @@
|
||||
aria-required={required}
|
||||
aria-invalid={invalid || !!error}
|
||||
aria-describedby={[error ? errorId : '', helpText ? helpId : ''].filter(Boolean).join(' ')}
|
||||
class="min-h-[120px] w-full resize-y rounded-lg border bg-white px-4 py-3 text-base text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
class="{sizeClasses} w-full resize-y border bg-white text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
? 'border-error ring-2 ring-error-subtle'
|
||||
: 'border-stein-300 hover:border-stein-400 focus:border-wald-500 focus:ring-2 focus:ring-wald-100'} disabled:bg-stein-100 disabled:opacity-60"
|
||||
></textarea>
|
||||
{#if error}
|
||||
<p id={errorId} class="mt-1.5 text-[0.8125rem] text-error" role="alert">
|
||||
<p id={errorId} class="mt-1 text-[0.75rem] text-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
{:else if helpText}
|
||||
<p id={helpId} class="mt-1.5 text-[0.8125rem] text-stein-500">
|
||||
<p id={helpId} class="mt-1 text-[0.75rem] text-stein-500">
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user