feat(auth): redesign auth UI to match Dart original

- Full-bleed bg_login.png background outside 500px MobileShell
- Facebook landing: logo + Facebook pill button + "Other Sign In Options" link
- Replace centered Dialog with true bottom sheet (drag handle, top radius 28px)
- Email panel: internal login/register toggle, no separate route
- Pink gradient primary button (#f96ADE → #f657A0)
- Pill input fields (46px, white, radius 24, pink 10% shadow)
- Pink circular checkbox + bold Privacy/Terms rich text (visual only, non-blocking)
- Floating 40x40 white back button (top:20/left:20)
- Wire AuthPanelModeChanged dispatch for panel switching
- New design tokens (--color-auth-*, --auth-*, --radius-bottom-sheet)
- New generic BottomSheet component
- Copy ic-logo-login.png (+@2x, +@3x) from Dart assets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:27:06 +08:00
parent 4e27b4c512
commit 90bb711a45
30 changed files with 967 additions and 335 deletions
+81 -11
View File
@@ -1,47 +1,117 @@
"use client";
/**
* Email 面板
* Email 面板login/register 内部切换)
*
* 原始 Dart: lib/ui/auth/widgets/auth_email_panel.dart
*
* 行为
* - 内部 `authMode`login/register)切换显示 Login / Register 表单
* - 提交派发 `AuthEmailLoginSubmitted` 或 `AuthEmailRegisterSubmitted`
* 视觉规格(与 Dart 对齐)
* - login 模式:顶部 24px spacer + logo120h + 24px gap
* - register 模式:48px spacer(两个 24px,无 logo
* - 表单(EmailLoginForm / EmailRegisterForm
* - 12px gap + 切换链接("Don't have an account? Sign up" / "Already have an account? Log in"
* - 16px gap + "Other Sign In Options" 链接(打开底部弹层)
* - Spacer + AuthLegalText
* - 顶部 "← Back" 按钮由父级 AuthPanel 渲染(悬浮 40×40 圆形)
*/
import { useState } from "react";
import { useAuthState } from "@/stores/auth/auth-context";
import { AuthErrorMessage } from "./auth-error-message";
import { AuthLegalText } from "./auth-legal-text";
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
import { EmailLoginForm } from "./email-login-form";
import { EmailRegisterForm } from "./email-register-form";
import styles from "./auth-email-panel.module.css";
export interface AuthEmailPanelProps {
onBack: () => void;
/** "Other Sign-in Options" 弹层中 Facebook 按钮回调(派发回 facebook 模式) */
onSwitchToFacebook: () => void;
/** "Other Sign-in Options" 弹层中 Google 按钮回调(可空;空时不渲染 Google 行) */
onGoogle?: () => void;
/** 弹层 Google 自定义子节点 */
googleSlot?: React.ReactNode;
}
export function AuthEmailPanel({ onBack }: AuthEmailPanelProps) {
export function AuthEmailPanel({
onSwitchToFacebook,
onGoogle,
googleSlot,
}: AuthEmailPanelProps) {
const state = useAuthState();
const [mode, setMode] = useState<"login" | "register">("login");
const [showOptions, setShowOptions] = useState(false);
return (
<div className={styles.panel}>
<button type="button" className={styles.backButton} onClick={onBack}>
Back
</button>
<div className={styles.topSection}>
{mode === "login" ? (
<>
<div className={styles.spacer24} />
<img
src="/images/auth/ic-logo-login.png"
alt="Cozsweet"
className={styles.logo}
/>
</>
) : (
<>
<div className={styles.spacer24} />
<div className={styles.spacer24} />
</>
)}
<div className={styles.spacer24} />
</div>
{mode === "login" ? (
<EmailLoginForm
onSwitchToRegister={() => setMode("register")}
globalError={state.errorMessage}
isLoading={state.isLoading}
/>
) : (
<EmailRegisterForm
onSwitchToLogin={() => setMode("login")}
globalError={state.errorMessage}
isLoading={state.isLoading}
/>
)}
<AuthErrorMessage message={state.errorMessage} />
<button
type="button"
className={styles.toggleButton}
onClick={() => setMode((m) => (m === "login" ? "register" : "login"))}
>
{mode === "login"
? "Don't have an account? Sign up"
: "Already have an account? Log in"}
</button>
<button
type="button"
className={styles.linkButton}
onClick={() => setShowOptions(true)}
>
Other Sign In Options
</button>
<div className={styles.spacer} />
<AuthLegalText />
<AuthOtherOptionsDialog
open={showOptions}
onClose={() => setShowOptions(false)}
mode="email"
onFacebook={onSwitchToFacebook}
onGoogle={() => {
setShowOptions(false);
onGoogle?.();
}}
onEmail={() => {
// 在 email 模式下不再显示 Email 入口
}}
googleSlot={googleSlot}
/>
</div>
);
}