58 lines
2.0 KiB
JavaScript
58 lines
2.0 KiB
JavaScript
import { useState } from 'react';
|
|
|
|
/**
|
|
* PasswordInput — a password field with an inline show/hide toggle.
|
|
* Accepts all standard <input> props plus className/style on the wrapper.
|
|
* Usage:
|
|
* <PasswordInput value={pw} onChange={e => setPw(e.target.value)} />
|
|
*/
|
|
export default function PasswordInput({ className, style, wrapperStyle, ...inputProps }) {
|
|
const [show, setShow] = useState(false);
|
|
|
|
return (
|
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', ...wrapperStyle }}>
|
|
<input
|
|
{...inputProps}
|
|
type={show ? 'text' : 'password'}
|
|
className={className ?? 'input'}
|
|
style={{ paddingRight: 38, width: '100%', ...style }}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShow(s => !s)}
|
|
tabIndex={-1}
|
|
aria-label={show ? 'Hide password' : 'Show password'}
|
|
style={{
|
|
position: 'absolute',
|
|
right: 10,
|
|
background: 'none',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
padding: 2,
|
|
color: 'var(--text-tertiary)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
lineHeight: 1,
|
|
}}
|
|
>
|
|
{show ? (
|
|
/* Eye-off icon */
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
|
|
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
|
|
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
</svg>
|
|
) : (
|
|
/* Eye icon */
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
<circle cx="12" cy="12" r="3"/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|