Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { type InputHTMLAttributes, forwardRef, useState } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface PasswordFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
|
||||
label: string;
|
||||
error?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/** Same visual language as `FormField`, plus a show/hide toggle - same
|
||||
* crossfade-icon-swap technique as `ApiKeyRow`'s Show/Hide button. */
|
||||
export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
|
||||
({ label, error, hint, id, className, ...props }, ref) => {
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const fieldId = id ?? props.name;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={fieldId} className="block text-sm font-medium text-slate-700">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<input
|
||||
ref={ref}
|
||||
id={fieldId}
|
||||
type={revealed ? "text" : "password"}
|
||||
className={clsx(
|
||||
"focus-ring block w-full rounded-md border border-slate-300 px-3 py-2 pr-10 text-sm shadow-sm transition-colors duration-150",
|
||||
error && "border-red-400",
|
||||
className,
|
||||
)}
|
||||
aria-invalid={error ? "true" : "false"}
|
||||
aria-describedby={error ? `${fieldId}-error` : hint ? `${fieldId}-hint` : undefined}
|
||||
{...props}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealed((r) => !r)}
|
||||
title={revealed ? "Hide password" : "Show password"}
|
||||
tabIndex={-1}
|
||||
className="focus-ring absolute inset-y-0 right-0 flex w-9 items-center justify-center text-slate-400 transition-colors duration-150 hover:text-slate-600"
|
||||
>
|
||||
<span className="relative block h-4 w-4">
|
||||
<Eye
|
||||
className={`absolute inset-0 h-4 w-4 transition-opacity duration-200 ${revealed ? "opacity-0" : "opacity-100"}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<EyeOff
|
||||
className={`absolute inset-0 h-4 w-4 transition-opacity duration-200 ${revealed ? "opacity-100" : "opacity-0"}`}
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{hint && !error && (
|
||||
<p id={`${fieldId}-hint`} className="mt-1 text-xs text-slate-500">
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p
|
||||
id={`${fieldId}-error`}
|
||||
className="mt-1 animate-fade-in text-xs text-red-600"
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
PasswordField.displayName = "PasswordField";
|
||||
Reference in New Issue
Block a user