184 lines
4.5 KiB
Plaintext
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>
|