- {/* Logo */}
-
-
-
-
- {loading() ?
- <>
-
-
Email Verified!
-
Redirecting you to the dashboard...
- >
- : <>
-
-
-
-
Check Your Email
-
We've sent a verification email to:
-
{email()}
+
+ {/* Logo */}
+
+
+
+
+ {loading() ?
+ <>
+
+
Email Verified!
+
Redirecting you to the dashboard...
+ >
+ : <>
+
-
-
- Click the verification link in your email to activate your account. Once verified,
- you'll automatically be redirected to the dashboard.
-
+
+
Check Your Email
+
We've sent a verification email to:
+
{email()}
+
-
+
+
+ Click the verification link in your email to activate your account. Once verified,
+ you'll automatically be redirected to the dashboard.
+
- {resent() && (
-
- Verification email sent successfully!
-
- )}
-
+
-
-
- Resend Email
-
-
-
Back to Sign In
-
-
-
-
Didn't receive the email? Check your spam folder or try resending.
-
- >
- }
-
+ {resent() && (
+
+ Verification email sent successfully!
+
+ )}
+
+
+
+
+ Resend Email
+
+
+
Back to Sign In
+
+
+
+
Didn't receive the email? Check your spam folder or try resending.
+
+ >
+ }
);
}
diff --git a/packages/web/src/components/auth/CompleteProfile.jsx b/packages/web/src/components/auth/CompleteProfile.jsx
index ba1d5da11..5dcda09bd 100644
--- a/packages/web/src/components/auth/CompleteProfile.jsx
+++ b/packages/web/src/components/auth/CompleteProfile.jsx
@@ -266,287 +266,281 @@ export default function CompleteProfile() {
const displayError = () => error();
return (
-
-
- }
- >
-
- {/* Logo */}
-
-
-
-
-
- {/* Step Indicator */}
-
-
- {(stepInfo, index) => (
-
- currentStep() + 1}
- >
-
-
-
-
-
-
- {stepInfo.title}
-
-
-
-
-
-
- )}
-
-
-
- {/* Step 1: Name and Title */}
-
-
-
- Complete Your Profile
-
-
- Just a few details to get you started
-
-
-
-
+
+
+ {/* Step 3: Persona Selection */}
+
+
+
+ What best describes you?
+
+
This helps us tailor your experience
+
+
+
+
+
+ {/* Completed Content (shows after all steps) */}
+
+
+
+
+
+
All Done!
+
Redirecting to your dashboard...
+
+
+
+
+
);
}
diff --git a/packages/web/src/components/auth/ErrorMessage.jsx b/packages/web/src/components/auth/ErrorMessage.jsx
index 639d4d0f0..3e4033470 100644
--- a/packages/web/src/components/auth/ErrorMessage.jsx
+++ b/packages/web/src/components/auth/ErrorMessage.jsx
@@ -1,9 +1,19 @@
import { AnimatedShow } from '../AnimatedShow.jsx';
+/**
+ * Error message component with proper accessibility attributes
+ * AnimatedShow already provides role="alert" and aria-live by default
+ * @param {Object} props
+ * @param {Function} props.displayError - Signal returning error string or null
+ * @param {string} props.id - Optional ID for aria-describedby linking
+ */
export default function ErrorMessage(props) {
return (
-
-
+
+
{props.displayError()}
diff --git a/packages/web/src/components/auth/MagicLinkForm.jsx b/packages/web/src/components/auth/MagicLinkForm.jsx
index 78916868c..ea086fb5b 100644
--- a/packages/web/src/components/auth/MagicLinkForm.jsx
+++ b/packages/web/src/components/auth/MagicLinkForm.jsx
@@ -100,7 +100,7 @@ export default function MagicLinkForm(props) {
Click the link in the email to sign in. The link expires in 10 minutes.
-
+
@@ -159,10 +159,11 @@ export default function MagicLinkForm(props) {
id='magic-link-email'
placeholder='you@example.com'
disabled={loading()}
+ aria-describedby={displayError() ? 'magic-link-error' : undefined}
/>
-
+
{props.buttonText || 'Send Sign-In Link'}
diff --git a/packages/web/src/components/auth/ProtectedGuard.jsx b/packages/web/src/components/auth/ProtectedGuard.jsx
index 385ac58a7..d8f8f74bf 100644
--- a/packages/web/src/components/auth/ProtectedGuard.jsx
+++ b/packages/web/src/components/auth/ProtectedGuard.jsx
@@ -5,16 +5,16 @@ import { PageLoader } from '@/components/ui/spinner';
/**
* ProtectedGuard - For authenticated pages (profile, settings, admin, etc.)
- * Redirects guests to dashboard
+ * Redirects guests to signin
*/
export default function ProtectedGuard(props) {
const { isLoggedIn, authLoading } = useBetterAuth();
const navigate = useNavigate();
- // Redirect non-logged-in users to dashboard
+ // Redirect non-logged-in users to signin
createEffect(() => {
if (!authLoading() && !isLoggedIn()) {
- navigate('/dashboard', { replace: true });
+ navigate('/signin', { replace: true });
}
});
diff --git a/packages/web/src/components/auth/ResetPassword.jsx b/packages/web/src/components/auth/ResetPassword.jsx
index 9325c2f42..821e13a60 100644
--- a/packages/web/src/components/auth/ResetPassword.jsx
+++ b/packages/web/src/components/auth/ResetPassword.jsx
@@ -5,6 +5,12 @@ import { AnimatedShow } from '../AnimatedShow.jsx';
import ErrorMessage from './ErrorMessage.jsx';
import { PrimaryButton, AuthLink } from './AuthButtons.jsx';
import StrengthIndicator from './StrengthIndicator.jsx';
+import {
+ PasswordInput,
+ PasswordInputControl,
+ PasswordInputField,
+ PasswordInputVisibilityTrigger,
+} from '@/components/ui/password-input';
import { handleError } from '@/lib/error-utils.js';
const REDIRECT_DELAY_MS = 3000;
@@ -14,17 +20,15 @@ export default function ResetPassword() {
const token = () => searchParams.token;
return (
-
-
- {/* Logo */}
-
-
-
-
-
}>
-
-
-
+
+ {/* Logo */}
+
+
+
+
+
}>
+
+
);
}
@@ -203,21 +207,22 @@ function SetNewPasswordForm(props) {
@@ -228,20 +233,21 @@ function SetNewPasswordForm(props) {
>
Confirm Password
-
setConfirmPassword(e.target.value)}
- class='w-full rounded-lg border border-gray-300 py-2 pr-3 pl-3 text-xs transition focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none sm:pr-4 sm:pl-4 sm:text-sm'
- required
- id='confirm-password-input'
- placeholder='Confirm new password'
- disabled={loading()}
- />
+
+
+ setConfirmPassword(e.target.value)}
+ placeholder='Confirm new password'
+ aria-describedby={displayError() ? 'reset-password-error' : undefined}
+ />
+
+
+
-
+
Set Password
diff --git a/packages/web/src/components/auth/RoleSelector.jsx b/packages/web/src/components/auth/RoleSelector.jsx
index 1de134d92..eb331be4e 100644
--- a/packages/web/src/components/auth/RoleSelector.jsx
+++ b/packages/web/src/components/auth/RoleSelector.jsx
@@ -46,25 +46,31 @@ export function getRoleLabel(roleId) {
/**
* Role selection grid for sign up flow
+ * Uses radiogroup pattern for single-select accessibility
* @param {Object} props
* @param {string} props.selectedRole - Currently selected role ID
* @param {Function} props.onSelect - Callback when role is selected
*/
export default function RoleSelector(props) {
return (
-
+
{roleOption => (
diff --git a/packages/web/src/components/auth/SignIn.jsx b/packages/web/src/components/auth/SignIn.jsx
index 20582475b..6f73686cd 100644
--- a/packages/web/src/components/auth/SignIn.jsx
+++ b/packages/web/src/components/auth/SignIn.jsx
@@ -1,4 +1,4 @@
-import { createSignal, onCleanup, onMount, Show } from 'solid-js';
+import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useBetterAuth } from '@api/better-auth-store.js';
import {
@@ -29,6 +29,9 @@ export default function SignIn() {
const [orcidLoading, setOrcidLoading] = createSignal(false);
const [useMagicLink, setUseMagicLink] = createSignal(false);
const [showTwoFactor, setShowTwoFactor] = createSignal(false);
+ const [formHeight, setFormHeight] = createSignal('auto');
+ let passwordFormRef;
+ let magicLinkFormRef;
const navigate = useNavigate();
const { signin, signinWithGoogle, signinWithOrcid, authError, clearAuthError } = useBetterAuth();
@@ -63,6 +66,28 @@ export default function SignIn() {
});
});
+ // Update form height based on which form is active
+ const updateFormHeight = () => {
+ const activeRef = useMagicLink() ? magicLinkFormRef : passwordFormRef;
+ if (activeRef) {
+ setFormHeight(`${activeRef.offsetHeight}px`);
+ }
+ };
+
+ // Set initial height after mount when refs are ready
+ onMount(() => {
+ // Small delay to ensure refs are measured after initial render
+ requestAnimationFrame(updateFormHeight);
+ });
+
+ // Update height when switching forms
+ createEffect(() => {
+ // Track the signal
+ useMagicLink();
+ // Update after a frame to ensure layout is complete
+ requestAnimationFrame(updateFormHeight);
+ });
+
// Watch for auth errors from the store
const displayError = () => error() || authError();
@@ -149,152 +174,216 @@ export default function SignIn() {
}
return (
-
-
- {/* Logo */}
-
-
-
-
- {/* Two-Factor Verification */}
-
-
-
-
- {/* Normal Sign In */}
-
-
-
+ {/* Logo */}
+
+
+
+
+ {/* Two-Factor Verification */}
+
+
+
+
+ {/* Normal Sign In */}
+
+
+
+ Welcome Back
+
+
Sign in to your account.
+
+
+
+
+ {/* Toggle between password and magic link */}
+ {
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
+ e.preventDefault();
+ const goToMagicLink = e.key === 'ArrowRight';
+ setUseMagicLink(goToMagicLink);
+ document.getElementById(goToMagicLink ? 'tab-magic-link' : 'tab-password')?.focus();
+ }
+ }}
+ >
+ {/* Sliding indicator */}
+
+
+
+
+
+ {/* Sliding form container */}
+
+
+
+
+
+ 1}
+ />
+ 1}
+ />
+
+
+
+ Don't have an account?{' '}
+
{
+ e.preventDefault();
+ navigate('/signup');
+ }}
+ >
+ Sign Up
+
+
+
);
}
diff --git a/packages/web/src/components/auth/SignUp.jsx b/packages/web/src/components/auth/SignUp.jsx
index 851dd5c94..ef63d422d 100644
--- a/packages/web/src/components/auth/SignUp.jsx
+++ b/packages/web/src/components/auth/SignUp.jsx
@@ -108,79 +108,75 @@ export default function SignUp() {
}
return (
-
-
- {/* Logo */}
-
-
-
+
+ {/* Logo */}
+
+
+
+
+
+
Create an Account
+
Get started with CoRATES
+
-
-
- Create an Account
-
-
Get started with CoRATES
-
-
- {/* Social providers */}
-
- 1}
- />
- 1}
- />
-
-
-
-
-
-
- {/* Magic Link Form - simple email signup */}
-
+ 1}
/>
-
-
- By continuing, you agree to our{' '}
-
- Terms of Service
- {' '}
- and{' '}
-
- Privacy Policy
-
- .
-
-
-
- Already have an account?{' '}
-
{
- e.preventDefault();
- navigate('/signin');
- }}
- >
- Sign In
-
-
+ 1}
+ />
+
+
+
+
+
+
+ {/* Magic Link Form - simple email signup */}
+
+
+
+ By continuing, you agree to our{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+
+
+ Already have an account?{' '}
+
{
+ e.preventDefault();
+ navigate('/signin');
+ }}
+ >
+ Sign In
+
);
diff --git a/packages/web/src/components/auth/SocialAuthButtons.jsx b/packages/web/src/components/auth/SocialAuthButtons.jsx
index 78c3b4d33..ee61652f4 100644
--- a/packages/web/src/components/auth/SocialAuthButtons.jsx
+++ b/packages/web/src/components/auth/SocialAuthButtons.jsx
@@ -23,9 +23,13 @@ export function GoogleButton(props) {
>
}
+ fallback={

}
>
-
+
Continue with Google
@@ -37,12 +41,19 @@ export function GoogleButton(props) {
disabled={props.loading}
class={`${baseClass} p-3 sm:p-3.5`}
title='Continue with Google'
+ aria-label='Continue with Google'
>
}
+ fallback={
+

+ }
>
-
+
@@ -72,9 +83,13 @@ export function OrcidButton(props) {
>
}
+ fallback={

}
>
-
+
Continue with ORCID
@@ -86,12 +101,19 @@ export function OrcidButton(props) {
disabled={props.loading}
class={`${baseClass} p-3 sm:p-3.5`}
title='Continue with ORCID'
+ aria-label='Continue with ORCID'
>
}
+ fallback={
+

+ }
>
-
+
diff --git a/packages/web/src/components/auth/TwoFactorVerify.jsx b/packages/web/src/components/auth/TwoFactorVerify.jsx
index 3cded284a..a61c67e48 100644
--- a/packages/web/src/components/auth/TwoFactorVerify.jsx
+++ b/packages/web/src/components/auth/TwoFactorVerify.jsx
@@ -92,10 +92,11 @@ export default function TwoFactorVerify(props) {
disabled={loading()}
id='2fa-code'
autoFocus
+ aria-describedby={displayError() ? '2fa-error' : undefined}
/>
-
+
Verify
diff --git a/packages/web/src/components/settings/pages/AcademicInfoSection.jsx b/packages/web/src/components/settings/pages/AcademicInfoSection.jsx
index 861fb6fad..5978684c4 100644
--- a/packages/web/src/components/settings/pages/AcademicInfoSection.jsx
+++ b/packages/web/src/components/settings/pages/AcademicInfoSection.jsx
@@ -100,7 +100,7 @@ export default function AcademicInfoSection() {
};
return (
-
+