feat: add PasswordInput component with visibility toggle #9

Merged
me merged 1 commits from style/ui_enhancement into main 2026-05-12 02:30:30 +00:00
4 changed files with 1040 additions and 993 deletions
+19 -17
View File
@@ -1,6 +1,7 @@
import { HourglassSimpleMediumIcon } from "@phosphor-icons/react";
import { useAuth } from "../../hooks/useAuth";
import { Modal } from "../ui/Modal";
import { PasswordInput } from "../ui/PasswordInput";
export function PasskeyModal() {
const { unlock } = useAuth();
@@ -27,7 +28,7 @@ export function PasskeyModal() {
</p>
<div className="modal-action items-center gap-4">
<form
className="form-control w-full inline-flex"
className="form-control w-full"
onSubmit={async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
@@ -36,22 +37,23 @@ export function PasskeyModal() {
await unlock(password);
}}
>
<input
name="password"
required
type="password"
placeholder="password"
data-testid="passkey-input"
className="font-sans validator input input-bordered rounded-r-none"
/>
<div className="validator-message text-xs text-error"></div>
<button
type="submit"
data-testid="passkey-submit-btn"
className="btn btn-primary rounded-l-none"
>
Unlock
</button>
<div className="join w-full">
<PasswordInput
name="password"
required
placeholder="password"
data-testid="passkey-input"
className="rounded-r-none w-full"
/>
<button
type="submit"
data-testid="passkey-submit-btn"
className="btn btn-primary rounded-l-none"
>
Unlock
</button>
</div>
<div className="validator-message text-xs text-error mt-2"></div>
</form>
</div>
</Modal>
+25 -13
View File
@@ -1,4 +1,5 @@
import type { UseFormRegisterReturn } from "react-hook-form";
import { PasswordInput } from "./PasswordInput";
interface FormFieldProps {
label: string;
@@ -20,25 +21,36 @@ export default function FormField({
"data-testid": testId,
}: FormFieldProps) {
return (
<div className="form-control">
<div className="form-control w-full">
<label
htmlFor={registration.name}
className="field-label font-display text-neutral-content/80 font-medium"
>
{label}
</label>
<input
{...registration}
id={registration.name}
data-testid={testId}
type={type}
placeholder={placeholder}
className={`input input-bordered focus:input-primary ${
error ? "input-error" : ""
}`}
onFocus={handleFocus}
/>
{error && <p className="text-error">{error}</p>}
{type === "password" ? (
<PasswordInput
{...registration}
id={registration.name}
data-testid={testId}
placeholder={placeholder}
error={!!error}
onFocus={handleFocus}
/>
) : (
<input
{...registration}
id={registration.name}
data-testid={testId}
type={type}
placeholder={placeholder}
className={`input input-bordered focus:input-primary w-full ${
error ? "input-error" : ""
}`}
onFocus={handleFocus}
/>
)}
{error && <p className="mt-1 text-xs text-error font-medium">{error}</p>}
</div>
);
}
@@ -0,0 +1,37 @@
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useState } from "react";
interface PasswordInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
error?: boolean;
}
export function PasswordInput({
className,
error,
...props
}: PasswordInputProps) {
const [showPassword, setShowPassword] = useState(false);
return (
<div className="relative w-full">
<input
{...props}
type={showPassword ? "text" : "password"}
className={`input input-bordered focus:input-primary w-full pr-12 ${error ? "input-error" : ""
} ${className}`}
/>
<button
type="button"
className="absolute right-4 top-1/2 -translate-y-1/2 text-neutral-content/40 hover:text-primary transition-all duration-300 cursor-pointer"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeSlashIcon size={22} weight="duotone" />
) : (
<EyeIcon size={22} weight="duotone" />
)}
</button>
</div>
);
}
File diff suppressed because it is too large Load Diff