Skip to content

Commit 22f39f1

Browse files
🎨 Palette: Add password visibility toggle (#212)
* feat: add password visibility toggle to Input component - Added Eye/EyeOff icons from lucide-react - Implemented state to toggle input type - Added proper aria-labels for accessibility - Styled for both Neo and Glass themes * fix: associate label with input using useId - Addressed review comment to improve accessibility - Used React.useId() to generate unique IDs for inputs - Linked label and input via htmlFor/id attributes - Preserved existing ID prop behavior * fix: destructure id prop in Input component - Destructure id from props to prevent override issues - Ensure inputId is correctly determined from props or generated - Clean up props spreading on input element --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent a035ec0 commit 22f39f1

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

‎.Jules/palette.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Palette Journal

‎web/components/ui/Input.tsx‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React from 'react';
1+
import React, { useState, useId } from 'react';
2+
import { Eye, EyeOff } from 'lucide-react';
23
import { useTheme } from '../../contexts/ThemeContext';
34
import { THEMES } from '../../constants';
45

@@ -7,8 +8,14 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
78
error?: string;
89
}
910

10-
export const Input: React.FC<InputProps> = ({ label, error, className = '', ...props }) => {
11+
export const Input: React.FC<InputProps> = ({ label, error, className = '', type, id, ...props }) => {
1112
const { style, mode } = useTheme();
13+
const [showPassword, setShowPassword] = useState(false);
14+
const generatedId = useId();
15+
const inputId = id || generatedId;
16+
17+
const isPassword = type === 'password';
18+
const inputType = isPassword ? (showPassword ? 'text' : 'password') : type;
1219

1320
let inputStyles = "w-full outline-none transition-all duration-200";
1421

@@ -18,10 +25,35 @@ export const Input: React.FC<InputProps> = ({ label, error, className = '', ...p
1825
inputStyles += ` p-3 rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm focus:bg-white/10 focus:border-blue-400 focus:ring-2 focus:ring-blue-500/20 ${mode === 'dark' ? 'text-white placeholder-white/40' : 'text-gray-900 placeholder-gray-500'}`;
1926
}
2027

28+
if (isPassword) {
29+
inputStyles += " pr-10";
30+
}
31+
2132
return (
2233
<div className="flex flex-col gap-1 w-full">
23-
{label && <label className={`text-sm font-semibold ${style === THEMES.NEOBRUTALISM ? 'uppercase' : 'ml-1 opacity-80'}`}>{label}</label>}
24-
<input className={`${inputStyles} ${className}`} {...props} />
34+
{label && <label htmlFor={inputId} className={`text-sm font-semibold ${style === THEMES.NEOBRUTALISM ? 'uppercase' : 'ml-1 opacity-80'}`}>{label}</label>}
35+
<div className="relative">
36+
<input
37+
id={inputId}
38+
type={inputType}
39+
className={`${inputStyles} ${className}`}
40+
{...props}
41+
/>
42+
{isPassword && (
43+
<button
44+
type="button"
45+
onClick={() => setShowPassword(!showPassword)}
46+
className={`absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-md transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-1 ${
47+
style === THEMES.NEOBRUTALISM
48+
? (mode === 'dark' ? 'text-white opacity-80 focus:ring-white' : 'text-black opacity-60 focus:ring-black')
49+
: 'text-white/60 hover:text-white focus:ring-white/50'
50+
}`}
51+
aria-label={showPassword ? "Hide password" : "Show password"}
52+
>
53+
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
54+
</button>
55+
)}
56+
</div>
2557
{error && <span className="text-red-500 text-xs font-bold mt-1">{error}</span>}
2658
</div>
2759
);

0 commit comments

Comments
 (0)