From e8a2fe0fb800c889fccbd8b83740355dddae14ea Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 31 Dec 2025 06:14:32 +0000 Subject: [PATCH] feat: add loading state to auth buttons and improve field labels - Add `Spinner` component for consistent loading states. - Update `Button` component to support `isLoading` prop. - Add `label` prop to `Input` in `Auth.tsx` for better accessibility and clarity. - Implement loading state during login/signup to prevent double submissions. - Update `.Jules/palette.md` with accessibility learnings. --- .Jules/palette.md | 4 ++++ web/components/ui/Button.tsx | 7 +++++++ web/components/ui/Spinner.tsx | 23 +++++++++++++++++++++++ web/pages/Auth.tsx | 13 ++++++++----- 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 web/components/ui/Spinner.tsx diff --git a/.Jules/palette.md b/.Jules/palette.md index bdd475d..06e7a39 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -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. diff --git a/web/components/ui/Button.tsx b/web/components/ui/Button.tsx index cfccd8e..66ea2e8 100644 --- a/web/components/ui/Button.tsx +++ b/web/components/ui/Button.tsx @@ -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 { variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; } export const Button: React.FC = ({ children, variant = 'primary', size = 'md', + isLoading = false, className = '', + disabled, ...props }) => { const { style } = useTheme(); @@ -47,8 +51,11 @@ export const Button: React.FC = ({ return ( ); diff --git a/web/components/ui/Spinner.tsx b/web/components/ui/Spinner.tsx new file mode 100644 index 0000000..dfb4e1a --- /dev/null +++ b/web/components/ui/Spinner.tsx @@ -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 = ({ + size = 20, + className = '', + ariaLabel = 'Loading' +}) => { + return ( + + ); +}; diff --git a/web/pages/Auth.tsx b/web/pages/Auth.tsx index 180001c..99f74a5 100644 --- a/web/pages/Auth.tsx +++ b/web/pages/Auth.tsx @@ -214,7 +214,8 @@ export const Auth = () => { exit={{ height: 0, opacity: 0 }} > setName(e.target.value)} required @@ -225,16 +226,18 @@ export const Auth = () => { setEmail(e.target.value)} required className={isNeo ? 'rounded-none' : ''} /> setPassword(e.target.value)} required @@ -251,8 +254,8 @@ export const Auth = () => { )} -