justin.deal/src/components/common/FormInput.astro

184 lines
4.5 KiB
Plaintext

---
interface Props {
type?: string;
name: string;
label: string;
placeholder?: string;
required?: boolean;
pattern?: string;
minlength?: number;
maxlength?: number;
min?: number;
max?: number;
value?: string | number;
helperText?: string;
errorMessage?: string;
class?: string;
}
const {
type = 'text',
name,
label,
placeholder = '',
required = false,
pattern,
minlength,
maxlength,
min,
max,
value = '',
helperText = '',
errorMessage = '',
class: className = '',
} = Astro.props;
const id = `input-${name}`;
---
<div class:list={['form-field', className]}>
<label
for={id}
class="block text-sm font-medium mb-1 transition-all duration-200 form-label"
>
{label}{required && <span class="text-zag-button-red ml-1">*</span>}
</label>
<div class="relative">
<input
id={id}
type={type as any}
name={name}
placeholder={placeholder}
required={required}
pattern={pattern}
minlength={minlength}
maxlength={maxlength}
min={min}
max={max}
value={value}
class="form-input w-full px-3 py-2 border-2 border-solid rounded-lg focus:outline-none focus:ring-0 transition-all duration-200 zag-bg zag-text"
/>
<div class="validation-icon absolute right-3 top-1/2 transform -translate-y-1/2 opacity-0">
<!-- Valid icon -->
<svg
class="valid-icon h-5 w-5 text-green-500 hidden"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<!-- Invalid icon -->
<svg
class="invalid-icon h-5 w-5 text-red-500 hidden"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<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"></path>
</svg>
</div>
</div>
<!-- Helper text -->
<p class="helper-text mt-1 text-xs text-zag-text-muted">
{helperText}
</p>
<!-- Error message -->
<p class="error-text mt-1 text-xs text-zag-button-red hidden">
{errorMessage || 'Please enter a valid value'}
</p>
</div>
<style>
.form-field {
margin-bottom: 1.5rem;
}
.form-input:focus {
border-color: var(--color-zag-accent-dark);
}
.form-input:focus + .validation-icon {
opacity: 0;
}
.form-input:focus ~ .form-label {
color: var(--color-zag-accent-dark);
}
.form-input:valid:not(:focus):not(:placeholder-shown) {
border-color: #10b981; /* green-500 */
}
.form-input:invalid:not(:focus):not(:placeholder-shown) {
border-color: var(--color-zag-button-red);
}
.form-input:valid:not(:focus):not(:placeholder-shown) + .validation-icon .valid-icon {
display: block;
}
.form-input:invalid:not(:focus):not(:placeholder-shown) + .validation-icon .invalid-icon {
display: block;
}
.form-input:valid:not(:focus):not(:placeholder-shown) + .validation-icon,
.form-input:invalid:not(:focus):not(:placeholder-shown) + .validation-icon {
opacity: 1;
}
.form-input:invalid:not(:focus):not(:placeholder-shown) ~ .helper-text {
display: none;
}
.form-input:invalid:not(:focus):not(:placeholder-shown) ~ .error-text {
display: block;
}
/* Animation for validation icons */
.validation-icon svg {
transform-origin: center;
animation: pop 0.3s ease;
}
@keyframes pop {
0% { transform: scale(0.8) translateY(-50%); opacity: 0; }
100% { transform: scale(1) translateY(-50%); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.validation-icon svg {
animation: none;
}
}
</style>
<script>
// Add client-side validation
document.addEventListener('DOMContentLoaded', () => {
const formInputs = document.querySelectorAll('.form-input');
formInputs.forEach(input => {
const formField = input.closest('.form-field');
const label = formField?.querySelector('label');
input.addEventListener('focus', () => {
if (label) {
label.classList.add('text-zag-accent-dark');
}
});
input.addEventListener('blur', () => {
if (label) {
label.classList.remove('text-zag-accent-dark');
}
});
});
});
</script>