115 lines
3.2 KiB
Plaintext
115 lines
3.2 KiB
Plaintext
---
|
|
/**
|
|
* PasswordInput Pattern
|
|
* Composition example: Input with show/hide password toggle.
|
|
* Demonstrates building interactive patterns from UI primitives.
|
|
*/
|
|
import type { HTMLAttributes } from 'astro/types';
|
|
import { cn } from '@/lib/cn';
|
|
import { generateId } from '@/lib/utils';
|
|
import { inputVariants, inputSizeConfig } from '@/components/ui/form/Input/input.variants';
|
|
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
|
|
|
interface Props extends HTMLAttributes<'input'> {
|
|
label?: string;
|
|
error?: string;
|
|
hint?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
placeholder?: string;
|
|
class?: string;
|
|
id?: string;
|
|
autocomplete?: string;
|
|
}
|
|
|
|
const {
|
|
label,
|
|
error,
|
|
hint,
|
|
size = 'md',
|
|
placeholder = 'Enter password',
|
|
autocomplete = 'current-password',
|
|
class: className,
|
|
id,
|
|
...rest
|
|
} = Astro.props;
|
|
|
|
const inputId = id || generateId('password');
|
|
const config = inputSizeConfig[size];
|
|
|
|
const inputStyles = cn(
|
|
inputVariants({ size }),
|
|
error && 'border-destructive focus-visible:ring-destructive',
|
|
config.baseLeftPadding,
|
|
config.trailingPadding
|
|
);
|
|
---
|
|
|
|
<div class={cn('space-y-1.5', className)}>
|
|
{label && (
|
|
<label for={inputId} class="text-sm font-medium leading-none">
|
|
{label}
|
|
</label>
|
|
)}
|
|
|
|
<div class="relative">
|
|
<input
|
|
type="password"
|
|
id={inputId}
|
|
class={inputStyles}
|
|
placeholder={placeholder}
|
|
autocomplete={autocomplete}
|
|
aria-invalid={error ? 'true' : undefined}
|
|
aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
|
|
data-password-input
|
|
{...rest}
|
|
/>
|
|
<button
|
|
type="button"
|
|
class={cn(
|
|
'absolute right-0 top-0 flex items-center justify-center h-full',
|
|
'text-foreground-muted hover:text-foreground transition-colors',
|
|
config.iconWrapper
|
|
)}
|
|
data-password-toggle
|
|
aria-label="Toggle password visibility"
|
|
>
|
|
<span data-icon-show><Icon name="eye" size="sm" /></span>
|
|
<span data-icon-hide class="hidden"><Icon name="eye-off" size="sm" /></span>
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<p id={`${inputId}-error`} class="text-sm text-destructive">{error}</p>
|
|
)}
|
|
|
|
{hint && !error && (
|
|
<p id={`${inputId}-hint`} class="text-sm text-muted-foreground">{hint}</p>
|
|
)}
|
|
</div>
|
|
|
|
<script>
|
|
function initPasswordInputs() {
|
|
document.querySelectorAll('[data-password-toggle]').forEach((el) => {
|
|
const btn = el as HTMLElement;
|
|
if (btn.dataset.initialized) return;
|
|
btn.dataset.initialized = 'true';
|
|
|
|
btn.addEventListener('click', () => {
|
|
const input = btn.parentElement?.querySelector('[data-password-input]') as HTMLInputElement;
|
|
const showIcon = btn.querySelector('[data-icon-show]') as HTMLElement;
|
|
const hideIcon = btn.querySelector('[data-icon-hide]') as HTMLElement;
|
|
|
|
if (!input || !showIcon || !hideIcon) return;
|
|
|
|
const isPassword = input.type === 'password';
|
|
input.type = isPassword ? 'text' : 'password';
|
|
showIcon.classList.toggle('hidden', isPassword);
|
|
hideIcon.classList.toggle('hidden', !isPassword);
|
|
});
|
|
});
|
|
}
|
|
|
|
initPasswordInputs();
|
|
document.addEventListener('astro:page-load', initPasswordInputs);
|
|
</script>
|