Skip to content

Commit 49aba82

Browse files
fix(auth): sign-in, password reset and MFA set-up work at phone width (#29136)
Fixes #29080 Clause-②: no Metadata-protocol: none Part of #29040: the Auth flow child of the phone-width audit. Claim 6571286397, surface amendment 6571574347. ## 维护者速读 - **改了什么**:手机宽度(`sm` 断点以下)的登录、忘记密码、重置密码页和强制改密/MFA 遮罩:所有控件改为至少 44 px 的触控目标,登录与忘记密码的输入框字号改为 16 px,遮罩改为可滚动。桌面尺寸不变。 - **为什么改**:在 375 与 390 px 实测(showcase 与 CRM 两个示例):控件只有 28 到 40 px 高,文字链接 14 到 17 px;14 px 的输入框让 iOS 聚焦时整页放大;MFA 设置的扫码步骤在键盘弹出后标题被切掉、"Verify & continue" 按钮落在键盘下方且无法滚动到,用户无法完成 MFA 注册。 - **风险与回滚**:只是 Tailwind 类名,全部以 `sm:` 收回,桌面 1280 px 实测尺寸与之前一致。回滚即 revert 本 PR。 - **先看**:`ui/packages/app-shell/src/console/RemediationOverlay.tsx`(遮罩改为外层滚动、内层 `min-h-full` 居中);`ui/packages/auth/src/authStyles.tsx`(共享输入框与主按钮类);`ui/packages/auth/src/LoginForm.tsx`(显示密码按钮在手机上 44 px)。 - **两分钟验证**:手机宽度(375 px)打开 Console `/login`,所有输入框和按钮高 44 px,点输入框不放大;桌面打开同一页,外观与之前相同。 - **需要你做的**:无,席位落地。 ## What changes Below the `sm` breakpoint only: - **Touch targets.** `AUTH_INPUT_CLASS` and `AUTH_PRIMARY_BUTTON_CLASS` gain `min-h-[44px] sm:min-h-0`. The forms' secondary controls (text buttons, the "Get code" button, the SSO button, "Forgot password?", "Back to sign in") take the same floor through a module-internal `AUTH_TOUCH_TARGET_CLASS`, not exported from the package entry. The show-password toggle is `size-11` (`sm:size-7`) with the field's right padding widened to match. On the Console pages: the dev-admin chip, the Privacy and Terms links, the "Powered by ObjectStack" link, the reset-password fields and button, and the locale button (grown by a child selector in `AuthFooter`, so `LocaleSwitcher` itself is untouched). Links inside a sentence ("Sign up", "Sign in", "Request a new link") keep their size under WCAG 2.5.5's inline exception. - **Input text.** `AUTH_INPUT_CLASS` is `text-base sm:text-sm`, so the sign-in and forgot-password fields are 16 px on a phone and WebKit no longer zooms the page on focus. (The reset page and the overlay already use the 16 px components `Input`.) - **The remediation overlay scrolls.** It was `fixed inset-0 flex items-center` with no overflow. The overlay is now `overflow-y-auto` and centres its card inside a `min-h-full` column, so a card taller than the screen keeps its top and its submit reachable. Below `sm` its buttons, inputs and the backup-codes summary are 44 px. No shared layout file (ConsoleLayout, AppShell, UnifiedSidebar, LocaleSwitcher) is touched. No export, prop or key is added, so the declarations are `Clause-②: no` and `Metadata-protocol: none`, as claimed. ## Run record Tree `6b3ba82c` (origin/main), full `turbo run build`, `objectstack dev --seed-admin --fresh` for **app-showcase** and **app-crm**, the Console from `ui/apps/console` source through Vite, Playwright Chromium with `isMobile` + `hasTouch`. MFA was driven for real: `PUT /api/settings/auth` with `mfa_required: true` and `mfa_grace_period_days: 0`, so data reads answer `403 MFA_REQUIRED` and the Console raises its overlay; enrollment was completed with a TOTP computed from the served secret. Keyboard model: the layout viewport shrinks by the keyboard (336 px at 812, 346 px at 844, the WebView adjustResize case), each field is focused and scrolled into view, and the field and its submit button must sit in what is left. Showcase and CRM gave the same answer on every row. | Page | Check | 375 before | 375 after | 390 before | 390 after | |---|---|---|---|---|---| | /login | no horizontal scroll (also with the error banner) | PASS | PASS | PASS | PASS | | /login | touch targets 44 px | FAIL: fields and Sign In 40, toggle 28x28, Forgot password 16, dev chip 28, locale 32x32, Privacy/Terms 16, Powered-by 14 | PASS | FAIL (same) | PASS | | /login | keyboard: field clear and no focus zoom | FAIL: fields 14 px, iOS zooms | PASS: 16 px | FAIL | PASS | | /login | dialogs fit (locale menu) | PASS | PASS | PASS | PASS | | /login | long lists at 4x CPU | PASS: no long list; the 10-item locale menu opens in 170 to 504 ms on the Vite dev server | PASS | PASS | PASS | | /forgot-password | no horizontal scroll (form and sent state) | PASS | PASS | PASS | PASS | | /forgot-password | touch targets | FAIL: field and button 40, Back to sign in 17 | PASS | FAIL | PASS | | /forgot-password | keyboard | FAIL: field 14 px | PASS | FAIL | PASS | | /reset-password | no horizontal scroll | PASS | PASS | PASS | PASS | | /reset-password | touch targets | FAIL: fields and button 40 | PASS | FAIL | PASS | | /reset-password | keyboard | PASS | PASS | PASS | PASS | | MFA overlay, password step | scroll / fit / keyboard | PASS | PASS | PASS | PASS | | MFA overlay, password step | touch targets | FAIL: field and button 40, Sign out 16 | PASS | FAIL | PASS | | MFA overlay, scan step | keyboard | FAIL: card top at -138 px, Verify at 499 to 539 px of a 476 px viewport, nothing scrolls | PASS: overlay scrolls, Verify at 315 to 359 | FAIL (card top -127) | PASS | | MFA overlay, scan step | touch targets | FAIL: summary 16, field and button 40, Sign out 16 | PASS | FAIL | PASS | | 2FA sign-in, enrolled user | challenge reachable | FAIL, at any width | not in this PR (#29105) | FAIL | not in this PR | Desktop check after the fix, 1280 px: email and password fields 342x40 at 14 px, Sign In 342x40, toggle 28x28, dev chip 28 tall, locale 32x32, the sizes main draws. Screenshots before and after each fix, and the raw results, are in the project folder `issue-29080/` (not on GitHub, as the card asks). The register page under the examples' `invite_only` posture shows no form, so it was measured only for its links. ## Tests - New `ui/packages/auth/src/__tests__/phoneTouchTargets-29080.test.tsx`: every input and button of `LoginForm` (with SSO and phone OTP on) and `ForgotPasswordForm` carries the phone floor, the fields carry `text-base sm:text-sm`, the toggle is `size-11 sm:size-7`. - New `ui/packages/app-shell/src/console/__tests__/RemediationOverlay.phoneScroll-29080.test.tsx`: the overlay is `overflow-y-auto` and not `items-center`, its column is `min-h-full`, its card carries the phone floor. - Ablation, after committing the fix: overlay back to `flex items-center justify-center` without overflow, the overlay test fails (1 failed); the input class back to `h-10 ... text-sm`, the auth test fails (2 failed). `ablation-replace.mjs` restored each file to its HEAD blob with an empty `git diff HEAD`. <details><summary>Suites and type-check</summary> - `pnpm --dir ui exec vitest run packages/auth/` plus the 42 console and app-shell test files that name the touched pages and the overlay: Test Files 82 passed, Tests 572 passed. - `turbo run type-check --filter=@object-ui/auth --filter=@object-ui/app-shell --filter=@object-ui/console`: 73 tasks successful. </details> ## Acceptance notes - The card asks for a fix for every failure or a card for each one not fixed. All phone-width failures are fixed here. One failure is not a phone-width defect: a TOTP-enrolled user's password sign-in answers `twoFactorRedirect` and the Console stays on /login with no code field and no message, at any width. It is filed as #29105. - Playwright coverage at a phone viewport is optional for this flow (#29040 requires it for Approvals and Record only); the run record above was taken with Playwright against real stacks, and the class pins hold the fixes in CI. ## Verification All local readings at head `1f2e4483`. CI's check-runs carry every family CI runs; they are not re-run here. - `node scripts/pm/dispatch-gates.mjs --ran`: 34 derived, 34 run, 0 NOT-MEASURED, every exit 0 (`check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity` and `check-partof-closing-keyword` read this body). - `node scripts/check-sdui-manifest.mjs`: exit 0, no component config changed. - `pnpm --dir ui exec vitest run scripts/`: 169 files passed, 2 skipped (4900 tests). - `pnpm --dir ui check:new-line-citations`: 0 new citations. - `pnpm check:type-check-debt`: exit 0. - Dogfood battery: not owed, no `!` in the title and no `(narrowing)` arm. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jSjgRwjHh1CopZ7aDU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ac1c24 commit 49aba82

11 files changed

Lines changed: 247 additions & 33 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/auth': patch
3+
'@object-ui/app-shell': patch
4+
'@object-ui/console': patch
5+
---
6+
7+
fix(auth): sign-in, password reset and the authenticator set-up work at phone width
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
Measured in a real Console at 375 and 390 px, against the showcase and CRM examples. No page scrolled sideways, but three things failed on a phone, and all three are fixed. Above the `sm` breakpoint nothing changes.
13+
14+
- Every control on the sign-in, forgot-password and reset-password pages, and in the remediation overlay, was under the 44 px touch-target floor: 40 px fields and buttons, a 28 px show-password toggle, text links 14 to 17 px tall, the 32 px language button. Below `sm` each is now at least 44 px. Links inside a sentence keep their size.
15+
- The sign-in and forgot-password fields used 14 px text, so iOS zoomed the page each time one took focus. They use 16 px on a phone.
16+
- The authenticator set-up step of the remediation overlay (QR code, backup codes, code field) is taller than a phone screen once the keyboard is up, and the overlay did not scroll: the heading went off the top and the Verify button under the keyboard, out of reach. The overlay now scrolls.
17+
18+
What to do: nothing.

‎ui/apps/console/src/pages/auth/AuthFooter.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ export function readLegalLinks(cfg: AuthPublicConfig | null): { privacyUrl?: str
4444

4545
export function AuthFooter({ privacyUrl, termsUrl }: { privacyUrl?: string; termsUrl?: string }) {
4646
const { t } = useObjectTranslation();
47+
// On a phone (below `sm`) each control here is a 44 px touch target.
4748
const linkClass =
48-
'underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:underline';
49+
'inline-flex min-h-[44px] min-w-[44px] items-center justify-center underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:underline sm:min-h-0 sm:min-w-0';
4950
return (
5051
<div
5152
data-testid="auth-footer"
5253
className="flex min-h-8 items-center justify-between gap-3 px-1 text-xs text-muted-foreground"
5354
>
54-
<div className="flex items-center">
55+
{/* LocaleSwitcher draws a 32 px icon button; grow it to 44 px on a phone. */}
56+
<div className="flex items-center max-sm:[&_button]:size-11">
5557
<LocaleSwitcher />
5658
</div>
5759
{privacyUrl || termsUrl ? (

‎ui/apps/console/src/pages/auth/AuthLayout.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function AuthLayout({ children, formWidth = 'sm' }: AuthLayoutProps) {
180180
href={OBJECTSTACK_SITE_URL}
181181
target="_blank"
182182
rel="noopener noreferrer"
183-
className="underline-offset-4 hover:text-foreground hover:underline"
183+
className="inline-flex min-h-[44px] items-center underline-offset-4 hover:text-foreground hover:underline sm:min-h-0"
184184
>
185185
{t('auth.shell.poweredBy', { defaultValue: 'Powered by ObjectStack' })}
186186
</a>

‎ui/apps/console/src/pages/auth/LoginPage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ function DevAdminRow({ seed }: { seed: DevSeedAdmin }) {
440440
onClick={signInAsDevAdmin}
441441
disabled={pending}
442442
aria-busy={pending}
443-
className="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-60"
443+
className="inline-flex h-7 min-h-[44px] shrink-0 items-center gap-1.5 rounded-lg border sm:min-h-0 border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-60"
444444
>
445445
{pending ? <AuthSpinner /> : <WrenchIcon />}
446446
{t('auth.login.devAdminHint.signIn', { defaultValue: 'Sign in as dev admin' })}

‎ui/apps/console/src/pages/auth/ResetPasswordPage.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import {
2424
} from '@object-ui/components';
2525
import { AuthLayout } from './AuthLayout';
2626

27+
/** A phone (below `sm`) gets the 44 px touch-target floor; wider screens keep 40 px. */
28+
const PHONE_TARGET_CLASS = 'min-h-[44px] sm:min-h-0';
29+
2730
export function ResetPasswordPage() {
2831
const { t } = useObjectTranslation();
2932
const navigate = useNavigate();
@@ -106,6 +109,7 @@ export function ResetPasswordPage() {
106109
</Label>
107110
<Input
108111
id="new-password"
112+
className={PHONE_TARGET_CLASS}
109113
type="password"
110114
autoComplete="new-password"
111115
required
@@ -122,6 +126,7 @@ export function ResetPasswordPage() {
122126
</Label>
123127
<Input
124128
id="confirm-password"
129+
className={PHONE_TARGET_CLASS}
125130
type="password"
126131
autoComplete="new-password"
127132
required
@@ -130,7 +135,7 @@ export function ResetPasswordPage() {
130135
onChange={(e) => setConfirmPassword(e.target.value)}
131136
/>
132137
</div>
133-
<Button type="submit" className="w-full" disabled={submitting}>
138+
<Button type="submit" className={`w-full ${PHONE_TARGET_CLASS}`} disabled={submitting}>
134139
{submitting
135140
? t('auth.resetPassword.submitting', { defaultValue: 'Updating…' })
136141
: t('auth.resetPassword.submit', { defaultValue: 'Update password' })}

‎ui/packages/app-shell/src/console/RemediationOverlay.tsx‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,27 @@ export function RemediationOverlay() {
5959
// must not be shown the impersonated user's forms as if they were their own.
6060
const impersonating = Boolean(exit.impersonatedBy) || exit.stopping || restored;
6161
return (
62-
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-background/95 p-4 backdrop-blur-sm">
63-
<div className="w-full max-w-md rounded-xl border bg-card p-6 shadow-xl">
64-
{impersonating ? (
65-
<ImpersonationExitPanel exit={exit} passwordPosture={passwordPosture} />
66-
) : passwordPosture ? (
67-
<ChangePasswordForm
68-
forcedReset={remediationRequired.code === 'PASSWORD_RESET_REQUIRED'}
69-
message={remediationRequired.message}
70-
/>
71-
) : (
72-
<MfaEnrollForm message={remediationRequired.message} />
73-
)}
62+
// The overlay scrolls: on a phone the authenticator step (QR code, backup
63+
// codes, code field) is taller than what the keyboard leaves of the screen,
64+
// and a fixed, centred card would cut off its top and its submit button.
65+
// Below `sm` every control is a 44 px touch target.
66+
<div className="fixed inset-0 z-[200] overflow-y-auto bg-background/95 backdrop-blur-sm">
67+
<div className="flex min-h-full items-center justify-center p-4">
68+
<div
69+
data-testid="remediation-card"
70+
className="w-full max-w-md rounded-xl border bg-card p-6 shadow-xl max-sm:[&_button]:min-h-[44px] max-sm:[&_input]:min-h-[44px] max-sm:[&_summary]:leading-[44px]"
71+
>
72+
{impersonating ? (
73+
<ImpersonationExitPanel exit={exit} passwordPosture={passwordPosture} />
74+
) : passwordPosture ? (
75+
<ChangePasswordForm
76+
forcedReset={remediationRequired.code === 'PASSWORD_RESET_REQUIRED'}
77+
message={remediationRequired.message}
78+
/>
79+
) : (
80+
<MfaEnrollForm message={remediationRequired.message} />
81+
)}
82+
</div>
7483
</div>
7584
</div>
7685
);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* The remediation overlay at phone width (objectstack#29080).
3+
*
4+
* The authenticator step (QR code, backup codes, code field) is taller than a
5+
* phone screen once the keyboard is up. Measured at 375 x 812 the card's top
6+
* sat 138 px above the screen and Verify under the keyboard, and nothing
7+
* scrolled. The overlay now scrolls and centres its card inside a `min-h-full`
8+
* column, and below `sm` its controls are 44 px touch targets. jsdom lays
9+
* nothing out, so the classes that carry each rule are read.
10+
*/
11+
12+
import { describe, it, expect, vi, afterEach } from 'vitest';
13+
import { cleanup, render, screen } from '@testing-library/react';
14+
import { I18nProvider } from '@object-ui/i18n';
15+
16+
vi.mock('@object-ui/auth', async (importOriginal) => ({
17+
...(await importOriginal<typeof import('@object-ui/auth')>()),
18+
useAuth: () => ({
19+
remediationRequired: { code: 'MFA_REQUIRED', message: 'Set up an authenticator app.' },
20+
setRemediationRequired: vi.fn(),
21+
changePassword: vi.fn(),
22+
enrollTotp: vi.fn(),
23+
verifyTotp: vi.fn(),
24+
signOut: vi.fn(),
25+
}),
26+
}));
27+
28+
import { RemediationOverlay } from '../RemediationOverlay';
29+
30+
afterEach(cleanup);
31+
32+
const tokens = (el: Element | null) => (el?.getAttribute('class') ?? '').split(/\s+/);
33+
34+
describe('RemediationOverlay at phone width (objectstack#29080)', () => {
35+
it('scrolls, so a card taller than the screen keeps its top and its submit reachable', () => {
36+
render(
37+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }}>
38+
<RemediationOverlay />
39+
</I18nProvider>,
40+
);
41+
const card = screen.getByTestId('remediation-card');
42+
const column = card.parentElement;
43+
const overlay = column?.parentElement ?? null;
44+
45+
expect(tokens(overlay)).toEqual(expect.arrayContaining(['fixed', 'inset-0', 'overflow-y-auto']));
46+
// A centred flex overlay clips a tall card at both ends; the column is at
47+
// least the overlay's height and centres the card inside the scroll.
48+
expect(tokens(overlay)).not.toContain('items-center');
49+
expect(tokens(column)).toEqual(expect.arrayContaining(['flex', 'min-h-full', 'items-center']));
50+
51+
expect(tokens(card)).toEqual(
52+
expect.arrayContaining([
53+
'max-sm:[&_button]:min-h-[44px]',
54+
'max-sm:[&_input]:min-h-[44px]',
55+
'max-sm:[&_summary]:leading-[44px]',
56+
]),
57+
);
58+
// The password step is drawn inside the card.
59+
expect(card.querySelector('#rem-pw')).not.toBeNull();
60+
});
61+
});

‎ui/packages/auth/src/ForgotPasswordForm.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
AUTH_INPUT_CLASS,
1515
AUTH_LINK_CLASS,
1616
AUTH_PRIMARY_BUTTON_CLASS,
17+
AUTH_TOUCH_TARGET_CLASS,
1718
AuthErrorBanner,
1819
AuthFormHeader,
1920
AuthMailIcon,
@@ -273,7 +274,7 @@ export function ForgotPasswordForm({
273274
/>
274275
{loginUrl && (
275276
<p className="text-center text-sm text-muted-foreground">
276-
<LinkComp href={loginUrl} className={AUTH_LINK_CLASS}>
277+
<LinkComp href={loginUrl} className={`${AUTH_LINK_CLASS} inline-flex items-center ${AUTH_TOUCH_TARGET_CLASS}`}>
277278
← {l.backToSignInText}
278279
</LinkComp>
279280
</p>
@@ -299,7 +300,7 @@ export function ForgotPasswordForm({
299300
/>
300301
{loginUrl && (
301302
<p className="text-center text-sm text-muted-foreground">
302-
<LinkComp href={loginUrl} className={AUTH_LINK_CLASS}>
303+
<LinkComp href={loginUrl} className={`${AUTH_LINK_CLASS} inline-flex items-center ${AUTH_TOUCH_TARGET_CLASS}`}>
303304
← {l.backToSignInText}
304305
</LinkComp>
305306
</p>
@@ -360,7 +361,7 @@ export function ForgotPasswordForm({
360361
type="button"
361362
onClick={handleSendOtp}
362363
disabled={otpSending || otpCooldown > 0 || !phone.trim() || resetting}
363-
className="shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50"
364+
className={`shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50 ${AUTH_TOUCH_TARGET_CLASS}`}
364365
>
365366
{otpSending && <AuthSpinner />}
366367
{otpCooldown > 0
@@ -399,7 +400,7 @@ export function ForgotPasswordForm({
399400
<button
400401
type="button"
401402
onClick={() => switchMode('email')}
402-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
403+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
403404
>
404405
{l.useEmailResetText}
405406
</button>
@@ -434,7 +435,7 @@ export function ForgotPasswordForm({
434435
<button
435436
type="button"
436437
onClick={() => switchMode('phone')}
437-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
438+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
438439
>
439440
{l.usePhoneResetText}
440441
</button>

‎ui/packages/auth/src/LoginForm.tsx‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
AUTH_INPUT_CLASS,
1717
AUTH_LINK_CLASS,
1818
AUTH_PRIMARY_BUTTON_CLASS,
19+
AUTH_TOUCH_TARGET_CLASS,
1920
AuthErrorBanner,
2021
AuthFormHeader,
2122
AuthSpinner,
@@ -522,7 +523,7 @@ export function LoginForm({
522523
type="button"
523524
onClick={handleSendOtp}
524525
disabled={otpSending || otpCooldown > 0 || !phone.trim() || isLoading}
525-
className="shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50"
526+
className={`shrink-0 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50 ${AUTH_TOUCH_TARGET_CLASS}`}
526527
>
527528
{otpSending && <AuthSpinner />}
528529
{otpCooldown > 0
@@ -544,7 +545,7 @@ export function LoginForm({
544545
<button
545546
type="button"
546547
onClick={() => switchMode('password')}
547-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
548+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
548549
>
549550
{l.usePasswordSignInText}
550551
</button>
@@ -584,7 +585,7 @@ export function LoginForm({
584585
{forgotPasswordUrl && (
585586
<LinkComp
586587
href={forgotPasswordUrl}
587-
className="text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
588+
className={`inline-flex items-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
588589
>
589590
{l.forgotPasswordText}
590591
</LinkComp>
@@ -600,7 +601,7 @@ export function LoginForm({
600601
required
601602
autoComplete="current-password"
602603
disabled={isLoading}
603-
className={`${AUTH_INPUT_CLASS} pr-10`}
604+
className={`${AUTH_INPUT_CLASS} pr-12 sm:pr-10`}
604605
/>
605606
<button
606607
type="button"
@@ -611,7 +612,7 @@ export function LoginForm({
611612
aria-label={passwordVisible ? l.hidePasswordText : l.showPasswordText}
612613
aria-controls="login-password"
613614
disabled={isLoading}
614-
className="absolute right-1.5 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50"
615+
className="absolute right-0 top-1/2 inline-flex size-11 -translate-y-1/2 sm:right-1.5 sm:size-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50"
615616
>
616617
{passwordVisible ? <EyeOffIcon /> : <EyeIcon />}
617618
</button>
@@ -633,7 +634,7 @@ export function LoginForm({
633634
onClick={handleSso}
634635
disabled={isLoading || ssoSubmitting}
635636
aria-busy={ssoSubmitting}
636-
className="flex h-10 w-full items-center justify-center gap-2 rounded-lg border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50"
637+
className={`flex h-10 w-full items-center justify-center gap-2 rounded-lg border border-input bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent disabled:opacity-50 ${AUTH_TOUCH_TARGET_CLASS}`}
637638
>
638639
{ssoSubmitting && <AuthSpinner />}
639640
{l.ssoButton}
@@ -644,7 +645,7 @@ export function LoginForm({
644645
<button
645646
type="button"
646647
onClick={() => switchMode('phone-otp')}
647-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
648+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
648649
>
649650
{l.usePhoneOtpText}
650651
</button>
@@ -656,7 +657,7 @@ export function LoginForm({
656657
<button
657658
type="button"
658659
onClick={() => setShowPasswordFallback(false)}
659-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
660+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
660661
>
661662
{l.backToSsoText}
662663
</button>
@@ -672,7 +673,7 @@ export function LoginForm({
672673
<button
673674
type="button"
674675
onClick={() => setShowPasswordFallback(true)}
675-
className="w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
676+
className={`w-full text-center text-xs text-muted-foreground underline-offset-4 transition-colors hover:text-primary hover:underline ${AUTH_TOUCH_TARGET_CLASS}`}
676677
>
677678
{l.usePasswordText}
678679
</button>

0 commit comments

Comments
 (0)