refactor(auth): share the auth landing and extract AuthField
CI / check (push) Successful in 36s
CI / tests-imaging-browsers (push) Successful in 1m36s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 1m57s
CI / tests-quality (push) Successful in 52s
CI / tests-ui (push) Successful in 4m46s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m40s

The login and register pages were near-identical after the previous cleanup,
and both forms repeated the same labelled, icon-prefixed input seven times.
Move the data loading and page shell into auth-landing.tsx (loadAuthLanding +
AuthLandingView) and the input markup into a single AuthField, so the pages
become thin compositions and every field shares one focus, icon and aria
implementation. The numbered section headers collapse into AuthSectionHeader.

No behaviour change.
This commit is contained in:
openhands committed 2026-10-18 19:01:40 +02:00
1 parent c6e5e0b1a9
commit aef0939ee9
6 files changed
+436 -393

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type { CSSProperties, ReactNode, Ref } from "react";
const ICON_CLASS =
"pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2";
export interface AuthFieldProps {
id: string;
name: string;
label: string;
icon: LucideIcon;
type?: string;
/** Controlled value; omit both `value` and `onChange` for an uncontrolled field. */
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
autoComplete?: string;
inputMode?:
| "none"
| "text"
| "tel"
| "url"
| "email"
| "numeric"
| "decimal"
| "search";
required?: boolean;
disabled?: boolean;
/** Marks the field with `aria-invalid` and a danger border. */
invalid?: boolean;
/** id of the element describing this field (e.g. an error banner). */
describedBy?: string;
iconColor?: string;
inputStyle?: CSSProperties;
/** Absolutely positioned control inside the input, e.g. `PasswordToggle`. */
trailing?: ReactNode;
/** Content rendered below the field: strength meter, policy hints, mismatch. */
children?: ReactNode;
/** Extra classes on the wrapping `space-y` container. */
className?: string;
inputRef?: Ref<HTMLInputElement>;
}
/**
* One labelled, icon-prefixed auth input. Login and register repeat this markup
* seven times; centralising it keeps the icon, focus and `aria-describedby`
* behaviour identical everywhere.
*/
export function AuthField({
id,
name,
label,
icon: Icon,
type = "text",
value,
onChange,
placeholder,
autoComplete,
inputMode,
required,
disabled,
invalid,
describedBy,
iconColor = "var(--color-text-muted)",
inputStyle,
trailing,
children,
className = "",
inputRef,
}: AuthFieldProps) {
return (
<div className={`space-y-1.5 ${className}`}>
<label htmlFor={id} className="auth-label">
{label}
</label>
<div className="relative">
<Icon
aria-hidden="true"
className={ICON_CLASS}
style={{ color: iconColor }}
/>
<input
ref={inputRef}
id={id}
name={name}
type={type}
value={value}
onChange={onChange ? (e) => onChange(e.target.value) : undefined}
placeholder={placeholder}
autoComplete={autoComplete}
inputMode={inputMode}
required={required}
disabled={disabled}
aria-invalid={invalid ? true : undefined}
aria-describedby={describedBy}
className={`auth-input has-icon ${trailing ? "has-action" : ""}`}
style={inputStyle}
/>
{trailing}
</div>
{children}
</div>
);
}