Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Palette Journal

## 2024-05-22 - Password Toggles and Label Selectors
**Learning:** When implementing "Show/Hide Password" toggles within an input field, simply adding an icon button can create ambiguity for automated testing tools if not careful. While `htmlFor`/`id` binding connects the visual label to the input correctly, the toggle button's `aria-label` (e.g., "Show password") contains the same keyword ("password") as the main label.
**Action:** Always prefer selecting by strict label text or using specific input types (`input[type='password']`) in tests when a field has auxiliary interactive controls containing similar accessible names. Ensure the toggle button is clearly distinct from the input itself in the accessibility tree.
7 changes: 7 additions & 0 deletions web/components/ui/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React from 'react';
import { THEMES } from '../../constants';
import { useTheme } from '../../contexts/ThemeContext';
import { Spinner } from './Spinner';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
isLoading = false,
className = '',
disabled,
...props
}) => {
const { style } = useTheme();
Expand Down Expand Up @@ -47,8 +51,11 @@ export const Button: React.FC<ButtonProps> = ({
return (
<button
className={`${baseStyles} ${sizeStyles[size]} ${themeStyles} ${className}`}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading && <Spinner size={size === 'sm' ? 16 : 20} />}
{children}
</button>
);
Expand Down
23 changes: 23 additions & 0 deletions web/components/ui/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Loader2 } from 'lucide-react';

interface SpinnerProps {
size?: number;
className?: string;
ariaLabel?: string;
}

export const Spinner: React.FC<SpinnerProps> = ({
size = 20,
className = '',
ariaLabel = 'Loading'
}) => {
return (
<Loader2
size={size}
className={`animate-spin ${className}`}
aria-label={ariaLabel}
role="status"
/>
);
};
13 changes: 8 additions & 5 deletions web/pages/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export const Auth = () => {
exit={{ height: 0, opacity: 0 }}
>
<Input
placeholder="Full Name"
label="Full Name"
placeholder="e.g. John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
required
Expand All @@ -225,16 +226,18 @@ export const Auth = () => {
</AnimatePresence>

<Input
label="Email Address"
type="email"
placeholder="Email Address"
placeholder="e.g. [email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className={isNeo ? 'rounded-none' : ''}
/>
<Input
label="Password"
type="password"
placeholder="Password"
placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
Expand All @@ -251,8 +254,8 @@ export const Auth = () => {
</motion.div>
)}

<Button type="submit" disabled={loading} className={`w-full py-4 text-lg ${isNeo ? 'rounded-none' : ''}`}>
{loading ? 'Processing...' : isLogin ? 'Log In' : 'Create Account'} <ArrowRight size={20} />
<Button type="submit" isLoading={loading} className={`w-full py-4 text-lg ${isNeo ? 'rounded-none' : ''}`}>
{isLogin ? 'Log In' : 'Create Account'} {!loading && <ArrowRight size={20} />}
</Button>
</form>

Expand Down
Loading