Initial release — Astro Rocket v1.0.0
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
---
|
||||
/**
|
||||
* 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>
|
||||
Reference in New Issue
Block a user