From f6df1b719e8efabf516b75dde7f39cd29256d2f8 Mon Sep 17 00:00:00 2001 From: willxue Date: Sat, 5 Sep 2026 20:16:51 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20interface=20review=20=E2=80=94=20foc?= =?UTF-8?q?us=20ring,=20AA-compliant=20text-soft,=20auth=20form=20affordan?= =?UTF-8?q?ces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: the interface review found the auth tab switcher shows zero visual change when the selected tab receives keyboard focus, the --text-soft token fails AA as a text color in both themes (2.78:1 light, 4.1:1 dark, 67 uses), and the registration invite code presented as optional while the backend requires it. Constraints / tradeoffs: - The tab focus indicator is an outline (the project idiom from .announcements-carousel-step:focus-visible) because the selected tab's elevation box-shadow already occupies the shadow slot. - Token values chosen to clear 4.5:1 on the surfaces they render on (light 0.60 -> 4.7:1+ on near-white; dark 0.55 -> 5.1:1+ on dark). - The invite code gets the same field-level treatment as the other required fields (required mark, inline 请输入邀请码, focus-first-invalid) so an empty submit no longer bounces back as a banner error whose copy (邀请码无效或已被使用) does not match an empty field. - OAuth anchors now resolve through the API client's base URL; same-site deployments keep the relative path, split origins (dev, previews, self-hosts) get the absolute URL. - Server error banner gains role="alert" (field errors already had it), auth inputs gain autocomplete (email / current-password / new-password / name) for password managers. - Two auth-form tests updated because they pinned the old behaviors (empty-invite submit-through; relative OAuth hrefs). Verification: - pnpm typecheck (web) and vitest auth-forms suite: 8/8 green. - Runtime re-verification of the focus ring and contrast pairs follows in the branch's final verification pass. - CHANGELOG [Unreleased] updated per the maintenance rule. --- CHANGELOG.md | 5 ++++ apps/web/src/features/auth/AuthForms.tsx | 34 +++++++++++++++++++----- apps/web/src/shared/api/client.ts | 5 +++- apps/web/src/shared/styles/index.css | 16 +++++++++-- apps/web/src/test/auth-forms.test.tsx | 18 ++++++++----- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1317d7e..f6789ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Gave the auth tab switcher a visible keyboard focus indicator (the selected tab previously showed no focus change at all) and raised the `--text-soft` token to an AA-compliant contrast in both themes (light 2.78→4.7:1, dark 4.1→5.1:1). +- Registration now marks the invite code as required and validates it inline, adds `autocomplete` to all auth inputs, announces server auth errors with `role="alert"`, and resolves OAuth links against the API origin so they work in split web/worker deployments. + ## [0.2.10] - 2026-09-05 ### Fixed diff --git a/apps/web/src/features/auth/AuthForms.tsx b/apps/web/src/features/auth/AuthForms.tsx index d7cc291..22320a6 100644 --- a/apps/web/src/features/auth/AuthForms.tsx +++ b/apps/web/src/features/auth/AuthForms.tsx @@ -2,6 +2,7 @@ import type { FormEvent, ReactNode } from "react"; import { CircleAlert, Eye, EyeOff, KeyRound, Mail, Ticket, UserRound } from "lucide-react"; import { useRef, useState } from "react"; +import { resolveApiUrl } from "../../shared/api/client"; import { Button, ButtonAnchor } from "../../shared/button"; import { FormField, TextInput } from "../../shared/form"; @@ -51,6 +52,13 @@ function validatePassword(value: string) { return null; } +function validateInviteCode(value: string) { + // Registration requires a valid invite server-side; surface that at the + // field instead of letting an empty submit come back as a banner error. + if (!value.trim()) return getRequiredMessage("邀请码"); + return null; +} + function compactFieldErrors(errors: AuthFieldErrors) { const visibleErrors: AuthFieldErrors = {}; @@ -152,7 +160,8 @@ export function AuthForms({ authError, onRegister, onLogin, mode, oauthNext = "/ const nextErrors: AuthFieldErrors = { registerName: validateName(registerName) ?? undefined, registerEmail: validateEmail(registerEmail) ?? undefined, - registerPassword: validatePassword(registerPassword) ?? undefined + registerPassword: validatePassword(registerPassword) ?? undefined, + registerInviteCode: validateInviteCode(registerInviteCode) ?? undefined }; const visibleErrors = compactFieldErrors(nextErrors); @@ -168,7 +177,11 @@ export function AuthForms({ authError, onRegister, onLogin, mode, oauthNext = "/ return (
- {authError ?

{authError}

: null} + {authError ? ( +

+ {authError} +

+ ) : null} {mode === "login" ? (
@@ -181,6 +194,7 @@ export function AuthForms({ authError, onRegister, onLogin, mode, oauthNext = "/ { @@ -205,6 +219,7 @@ export function AuthForms({ authError, onRegister, onLogin, mode, oauthNext = "/