feat(auth): unify and polish login, register and homepage auth UI

Login and register each carried their own copy of the promo panel and the
error banner, so the two pages drifted apart and every caller had to wire
role="alert" by hand. Extract a shared AuthPromoCard and AuthAlert, use them
in both forms and for the login notices, and link errors to their fields via
aria-invalid / aria-describedby.

Focus now lands on the 2FA code field the moment it appears, the credential
fields lock while a request is in flight, and invalid inputs get a danger
tint. The homepage top bar was a byte-for-byte duplicate of AuthTopBar; it
now renders the shared component, and the compact homepage login uses the
tighter spacing its comment already promised.
This commit is contained in:
openhands committed 2026-10-18 18:49:18 +02:00
1 parent 2249303dbd
commit c6e5e0b1a9
8 files changed
+212 -293

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+14
View File
@@ -2893,6 +2893,20 @@ html {
0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent), 0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent),
0 0 24px -8px color-mix(in srgb, var(--color-primary) 60%, transparent); 0 0 24px -8px color-mix(in srgb, var(--color-primary) 60%, transparent);
} }
.auth-input[aria-invalid="true"]:not(:focus) {
border-color: color-mix(in srgb, var(--color-danger) 60%, transparent);
background: color-mix(
in srgb,
var(--color-danger) 6%,
color-mix(in srgb, var(--color-background) 88%, var(--color-surface))
);
}
.auth-input[aria-invalid="true"]:focus {
border-color: var(--color-danger);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-danger) 18%, transparent),
0 0 24px -8px color-mix(in srgb, var(--color-danger) 55%, transparent);
}
.auth-input:disabled { .auth-input:disabled {
opacity: 0.55; opacity: 0.55;
cursor: not-allowed; cursor: not-allowed;
+38
View File
@@ -0,0 +1,38 @@
"use client";
import type { ReactNode } from "react";
interface AuthAlertProps {
children: ReactNode;
/** Tint of the box: red for errors, green for confirmations. */
variant?: "error" | "success";
/** Extra classes, e.g. spacing (`mb-4`) or entrance animation. */
className?: string;
/** Optional id so inputs can point at the message via `aria-describedby`. */
id?: string;
}
/**
* The single status/error banner for the auth forms. Uses `role="alert"` for
* failures (screen readers announce them immediately) and `role="status"` for
* confirmations, so the two are wired correctly instead of every caller
* repeating the markup and risking an unannounced message.
*/
export function AuthAlert({
children,
variant = "error",
className = "",
id,
}: AuthAlertProps) {
return (
<p
id={id}
role={variant === "error" ? "alert" : "status"}
className={`auth-alert animate-fade-in-up m-0 ${
variant === "success" ? "auth-alert--success" : ""
} ${className}`}
>
{children}
</p>
);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -9
View File
@@ -18,6 +18,7 @@ import {
type RegisterState, type RegisterState,
register, register,
} from "@/actions/register"; } from "@/actions/register";
import { AuthAlert } from "@/components/auth/auth-alert";
import { PasswordToggle } from "@/components/auth/password-toggle"; import { PasswordToggle } from "@/components/auth/password-toggle";
import Link from "@/components/link"; import Link from "@/components/link";
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry"; import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
@@ -183,15 +184,7 @@ export function RegisterForm({
}} }}
className="flex flex-col gap-6" className="flex flex-col gap-6"
> >
{errorMessage && ( {errorMessage && <AuthAlert>{errorMessage}</AuthAlert>}
<p
role="alert"
aria-live="polite"
className="auth-alert animate-fade-in-up m-0"
>
{errorMessage}
</p>
)}
{referralCode && ( {referralCode && (
<div <div