Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d525fea931 | ||
|
|
5a7ec6c9ed | ||
|
|
9282a06a3c |
+84
-21
@@ -196,11 +196,16 @@ const trustStats = [
|
|||||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
/>
|
/>
|
||||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||||
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||||
|
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="name-error" data-error="name" role="alert">
|
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="name-error" data-error="name" role="alert">
|
||||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||||
@@ -231,11 +236,16 @@ const trustStats = [
|
|||||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||||
placeholder="john@example.com"
|
placeholder="john@example.com"
|
||||||
/>
|
/>
|
||||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||||
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||||
|
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="email-error" data-error="email" role="alert">
|
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="email-error" data-error="email" role="alert">
|
||||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||||
@@ -267,6 +277,16 @@ const trustStats = [
|
|||||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||||
placeholder="+1 (555) 123-4567"
|
placeholder="+1 (555) 123-4567"
|
||||||
/>
|
/>
|
||||||
|
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||||
|
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||||
|
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
||||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||||
@@ -303,6 +323,16 @@ const trustStats = [
|
|||||||
<option value="support">Technical Support</option>
|
<option value="support">Technical Support</option>
|
||||||
<option value="other">Other / General Inquiry</option>
|
<option value="other">Other / General Inquiry</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
|
||||||
|
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
|
||||||
|
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
|
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
|
||||||
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
@@ -341,18 +371,30 @@ const trustStats = [
|
|||||||
</label>
|
</label>
|
||||||
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
|
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<div class="relative">
|
||||||
id="message"
|
<textarea
|
||||||
name="message"
|
id="message"
|
||||||
required
|
name="message"
|
||||||
minlength="10"
|
required
|
||||||
maxlength="5000"
|
minlength="10"
|
||||||
rows="5"
|
maxlength="5000"
|
||||||
aria-required="true"
|
rows="5"
|
||||||
aria-describedby="message-error message-hint"
|
aria-required="true"
|
||||||
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
|
aria-describedby="message-error message-hint"
|
||||||
placeholder="Tell us about your project, goals, and timeline..."
|
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
|
||||||
></textarea>
|
placeholder="Tell us about your project, goals, and timeline..."
|
||||||
|
></textarea>
|
||||||
|
<div class="valid-check absolute top-3 right-3 flex items-center hidden" aria-label="Valid">
|
||||||
|
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-check absolute top-3 right-3 flex items-center hidden" aria-label="Invalid">
|
||||||
|
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p id="message-hint" class="mt-1 text-xs text-secondary-400">Minimum 10 characters. Be as detailed as possible for a faster, more accurate response.</p>
|
<p id="message-hint" class="mt-1 text-xs text-secondary-400">Minimum 10 characters. Be as detailed as possible for a faster, more accurate response.</p>
|
||||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="message-error" data-error="message" role="alert">
|
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="message-error" data-error="message" role="alert">
|
||||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||||
@@ -709,18 +751,34 @@ const trustStats = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form input states */
|
/* Form input states — WCAG AA compliant (≥ 4.5:1 contrast) */
|
||||||
.form-input.is-valid {
|
.form-input.is-valid {
|
||||||
border-color: #10b981;
|
border-color: #155724;
|
||||||
background-color: #f0fdf4;
|
background-color: #f0fff4;
|
||||||
|
color: #155724;
|
||||||
}
|
}
|
||||||
.form-input.is-invalid {
|
.form-input.is-invalid {
|
||||||
border-color: #ef4444;
|
border-color: #721c24;
|
||||||
background-color: #fef2f2;
|
background-color: #fff5f5;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
/* Dark mode validation states */
|
||||||
|
:global(.dark) .form-input.is-valid {
|
||||||
|
border-color: #48bb78;
|
||||||
|
background-color: rgba(72, 187, 120, 0.1);
|
||||||
|
color: #c6f6d5;
|
||||||
|
}
|
||||||
|
:global(.dark) .form-input.is-invalid {
|
||||||
|
border-color: #fc8181;
|
||||||
|
background-color: rgba(252, 129, 129, 0.1);
|
||||||
|
color: #fed7d7;
|
||||||
}
|
}
|
||||||
.form-input.is-valid ~ .valid-check {
|
.form-input.is-valid ~ .valid-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.form-input.is-invalid ~ .invalid-check {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
/* Budget option - checked state via JS since has-[:checked] may not work in all contexts */
|
/* Budget option - checked state via JS since has-[:checked] may not work in all contexts */
|
||||||
.budget-option.selected {
|
.budget-option.selected {
|
||||||
@@ -857,6 +915,7 @@ const trustStats = [
|
|||||||
const errorEl = document.querySelector(`[data-error="${fieldName}"]`);
|
const errorEl = document.querySelector(`[data-error="${fieldName}"]`);
|
||||||
const inputEl = document.getElementById(fieldName) as HTMLElement | null;
|
const inputEl = document.getElementById(fieldName) as HTMLElement | null;
|
||||||
const validCheck = inputEl?.parentElement?.querySelector('.valid-check') as HTMLElement | null;
|
const validCheck = inputEl?.parentElement?.querySelector('.valid-check') as HTMLElement | null;
|
||||||
|
const invalidCheck = inputEl?.parentElement?.querySelector('.invalid-check') as HTMLElement | null;
|
||||||
|
|
||||||
if (errorEl) errorEl.classList.toggle('hidden', isValid !== false);
|
if (errorEl) errorEl.classList.toggle('hidden', isValid !== false);
|
||||||
|
|
||||||
@@ -867,6 +926,9 @@ const trustStats = [
|
|||||||
if (validCheck) {
|
if (validCheck) {
|
||||||
validCheck.classList.toggle('hidden', isValid !== true);
|
validCheck.classList.toggle('hidden', isValid !== true);
|
||||||
}
|
}
|
||||||
|
if (invalidCheck) {
|
||||||
|
invalidCheck.classList.toggle('hidden', isValid !== false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateField(field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): boolean {
|
function validateField(field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): boolean {
|
||||||
@@ -1017,6 +1079,7 @@ const trustStats = [
|
|||||||
(input as HTMLElement).classList.remove('is-valid', 'is-invalid');
|
(input as HTMLElement).classList.remove('is-valid', 'is-invalid');
|
||||||
});
|
});
|
||||||
form.querySelectorAll('.valid-check').forEach((el) => el.classList.add('hidden'));
|
form.querySelectorAll('.valid-check').forEach((el) => el.classList.add('hidden'));
|
||||||
|
form.querySelectorAll('.invalid-check').forEach((el) => el.classList.add('hidden'));
|
||||||
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
||||||
if (charCount) charCount.textContent = '0 / 5000';
|
if (charCount) charCount.textContent = '0 / 5000';
|
||||||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||||||
|
|||||||
Reference in New Issue
Block a user