From 90bb711a450e5576e7a4aeb3a25014bf184f7ce4 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 10 Jun 2026 14:27:06 +0800 Subject: [PATCH] feat(auth): redesign auth UI to match Dart original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- public/images/auth/ic-logo-login.png | Bin 0 -> 5795 bytes public/images/auth/ic-logo-login@2x.png | Bin 0 -> 7343 bytes public/images/auth/ic-logo-login@3x.png | Bin 0 -> 10202 bytes .../_components/core/bottom-sheet.module.css | 34 +++++ src/app/_components/core/bottom-sheet.tsx | 75 ++++++++++ .../components/auth-email-panel.module.css | 72 ++++++++-- src/app/auth/components/auth-email-panel.tsx | 92 +++++++++++-- .../components/auth-error-message.module.css | 14 +- .../components/auth-facebook-panel.module.css | 100 +++++++++++++- .../auth/components/auth-facebook-panel.tsx | 90 ++++++------ .../components/auth-legal-text.module.css | 48 ++++++- src/app/auth/components/auth-legal-text.tsx | 58 ++++++-- .../auth-other-options-dialog.module.css | 59 +++++--- .../components/auth-other-options-dialog.tsx | 129 ++++++++++++++---- src/app/auth/components/auth-panel.module.css | 41 +++++- src/app/auth/components/auth-panel.tsx | 68 +++++++-- .../components/auth-primary-button.module.css | 48 ++++--- .../auth/components/auth-primary-button.tsx | 22 +-- .../auth/components/auth-screen.module.css | 23 ++++ src/app/auth/components/auth-screen.tsx | 38 ++---- .../components/auth-social-button.module.css | 43 +++--- .../auth/components/auth-social-button.tsx | 24 ++-- .../components/auth-text-field.module.css | 37 +++-- src/app/auth/components/email-form.module.css | 28 ++-- src/app/auth/components/email-login-form.tsx | 48 ++----- .../auth/components/email-register-form.tsx | 50 ++----- src/tokens/colors.css | 30 ++++ src/tokens/dimensions.css | 27 ++++ src/tokens/radius.css | 1 + src/tokens/typography.css | 3 + 30 files changed, 967 insertions(+), 335 deletions(-) create mode 100644 public/images/auth/ic-logo-login.png create mode 100644 public/images/auth/ic-logo-login@2x.png create mode 100644 public/images/auth/ic-logo-login@3x.png create mode 100644 src/app/_components/core/bottom-sheet.module.css create mode 100644 src/app/_components/core/bottom-sheet.tsx create mode 100644 src/app/auth/components/auth-screen.module.css diff --git a/public/images/auth/ic-logo-login.png b/public/images/auth/ic-logo-login.png new file mode 100644 index 0000000000000000000000000000000000000000..d5b13922b3463df69163ec4a16fc8cd6c555aaf8 GIT binary patch literal 5795 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 literal 0 HcmV?d00001 diff --git a/public/images/auth/ic-logo-login@2x.png b/public/images/auth/ic-logo-login@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5bf0e2795963ad3855fb46cfc9241cc9e27f4335 GIT binary patch literal 7343 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 literal 0 HcmV?d00001 diff --git a/public/images/auth/ic-logo-login@3x.png b/public/images/auth/ic-logo-login@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1c708486d0f3f1c5cb696bc5853e00924dd429 GIT binary patch literal 10202 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 literal 0 HcmV?d00001 diff --git a/src/app/_components/core/bottom-sheet.module.css b/src/app/_components/core/bottom-sheet.module.css new file mode 100644 index 00000000..2ecccd2d --- /dev/null +++ b/src/app/_components/core/bottom-sheet.module.css @@ -0,0 +1,34 @@ +/** + * 底部弹层样式 + * + * 原始 Dart: showModalBottomSheet(透明背景,底部白卡 + 顶部圆角 radius28) + */ +.scrim { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.panel { + position: relative; + width: 100%; + max-width: 500px; + background: var(--color-auth-surface); + color: var(--color-auth-text-primary); + border-radius: var(--radius-bottom-sheet) var(--radius-bottom-sheet) 0 0; + box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.18); + padding-bottom: env(safe-area-inset-bottom); + animation: slideUp 0.18s ease-out; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} diff --git a/src/app/_components/core/bottom-sheet.tsx b/src/app/_components/core/bottom-sheet.tsx new file mode 100644 index 00000000..b9b1a333 --- /dev/null +++ b/src/app/_components/core/bottom-sheet.tsx @@ -0,0 +1,75 @@ +"use client"; +/** + * 通用底部弹层(Bottom Sheet) + * + * 原始 Dart: 散落在各处的 `showModalBottomSheet`(auth_other_options_dialog 等) + * + * 设计目标: + * - portal 渲染到 body + * - 固定底部、最大宽度 500px、顶部圆角 `--radius-bottom-sheet` + * - ESC 关闭 / 点击 scrim 关闭 / 打开时锁定 body 滚动 + * - 简单 slide-up 动画(与原 Dart 行为接近) + * - 调用方只关心内容布局 + */ +import { type ReactNode, useEffect } from "react"; +import { createPortal } from "react-dom"; + +import styles from "./bottom-sheet.module.css"; + +export interface BottomSheetProps { + open: boolean; + onClose: () => void; + children: ReactNode; + /** 蒙层透明度(0-1)。默认 0.45。 */ + scrimOpacity?: number; + /** 禁用点击外部 / ESC 关闭。 */ + persistent?: boolean; + /** ARIA 标签。 */ + ariaLabel?: string; +} + +export function BottomSheet({ + open, + onClose, + children, + scrimOpacity = 0.45, + persistent = false, + ariaLabel, +}: BottomSheetProps) { + useEffect(() => { + if (!open || typeof document === "undefined") return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && !persistent) onClose(); + }; + document.addEventListener("keydown", onKey); + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = prevOverflow; + }; + }, [open, onClose, persistent]); + + if (!open || typeof document === "undefined") return null; + + return createPortal( +
{ + if (!persistent && e.target === e.currentTarget) onClose(); + }} + > +
e.stopPropagation()} + > + {children} +
+
, + document.body, + ); +} diff --git a/src/app/auth/components/auth-email-panel.module.css b/src/app/auth/components/auth-email-panel.module.css index c8dbcf02..bd95551f 100644 --- a/src/app/auth/components/auth-email-panel.module.css +++ b/src/app/auth/components/auth-email-panel.module.css @@ -1,21 +1,75 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_email_panel.dart + * + * 视觉规格(与 Dart 对齐): + * - 顶部:login 模式有 logo + 24px spacer;register 模式两个 24px spacer + * - 表单 + 12px gap + 切换链接 + 16px gap + 弹层链接 + * - Spacer + 法律 + * - 切换链接:14px 黑色下划线居中 + * - "Other Sign In Options" 链接:14px 黑色下划线居中 + */ .panel { display: flex; flex-direction: column; - gap: var(--spacing-md); + align-items: stretch; + flex: 1 1 auto; width: 100%; + min-height: 0; } -.backButton { - align-self: flex-start; +.topSection { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.spacer24 { + height: var(--spacing-xxl); +} + +.logo { + display: block; + height: var(--auth-logo-height); + width: auto; + max-width: 220px; + margin: 0 auto; + object-fit: contain; +} + +.spacer { + flex: 1 1 0; + min-height: var(--spacing-lg); +} + +.toggleButton { + align-self: center; background: none; border: none; - padding: 0; - color: var(--color-text-secondary); - font-size: var(--font-size-sm); + padding: var(--spacing-md) 0 0; + font-size: var(--font-size-md); + color: var(--color-auth-text-primary); + text-decoration: underline; + text-underline-offset: 3px; cursor: pointer; - transition: color 0.15s ease; + transition: opacity 0.15s ease; } -.backButton:hover { - color: var(--color-text-primary); +.toggleButton:hover { + opacity: 0.8; +} + +.linkButton { + align-self: center; + background: none; + border: none; + padding: var(--spacing-md) 0 var(--spacing-md); + font-size: var(--font-size-md); + color: var(--color-auth-text-primary); + text-decoration: underline; + text-underline-offset: 3px; + cursor: pointer; + transition: opacity 0.15s ease; +} + +.linkButton:hover { + opacity: 0.8; } diff --git a/src/app/auth/components/auth-email-panel.tsx b/src/app/auth/components/auth-email-panel.tsx index 15a0f3ba..7f9af2ae 100644 --- a/src/app/auth/components/auth-email-panel.tsx +++ b/src/app/auth/components/auth-email-panel.tsx @@ -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 + logo(120h) + 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 (
- +
+ {mode === "login" ? ( + <> +
+ Cozsweet + + ) : ( + <> +
+
+ + )} +
+
+ {mode === "login" ? ( setMode("register")} globalError={state.errorMessage} isLoading={state.isLoading} /> ) : ( setMode("login")} globalError={state.errorMessage} isLoading={state.isLoading} /> )} + + + + + + + +
+ + + setShowOptions(false)} + mode="email" + onFacebook={onSwitchToFacebook} + onGoogle={() => { + setShowOptions(false); + onGoogle?.(); + }} + onEmail={() => { + // 在 email 模式下不再显示 Email 入口 + }} + googleSlot={googleSlot} + />
); } diff --git a/src/app/auth/components/auth-error-message.module.css b/src/app/auth/components/auth-error-message.module.css index ac8d8eb4..608e3980 100644 --- a/src/app/auth/components/auth-error-message.module.css +++ b/src/app/auth/components/auth-error-message.module.css @@ -1,10 +1,16 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_error_message.dart + * + * 视觉规格(与 Dart 对齐): + * - 细小红字,无背景,无圆角,无边框 + * - 12px 居中 + */ .banner { width: 100%; - padding: var(--spacing-md); - border-radius: var(--radius-md); - background: rgba(255, 77, 79, 0.12); - border: var(--border-light) solid rgba(255, 77, 79, 0.3); + padding: 0; + background: transparent; + border: none; color: var(--color-error); font-size: var(--font-size-sm); line-height: 1.4; + text-align: center; } diff --git a/src/app/auth/components/auth-facebook-panel.module.css b/src/app/auth/components/auth-facebook-panel.module.css index 31c5fefd..2a156e2d 100644 --- a/src/app/auth/components/auth-facebook-panel.module.css +++ b/src/app/auth/components/auth-facebook-panel.module.css @@ -1,21 +1,111 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_facebook_panel.dart + * + * 视觉规格(与 Dart 对齐): + * - Spacer → logo(120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律 + * - Facebook 主按钮:46px 白胶囊 + Facebook "f" 图标(#1877f2) + * - 链接:14px 黑色下划线居中 + */ .panel { display: flex; flex-direction: column; + align-items: stretch; + flex: 1 1 auto; + width: 100%; + min-height: 0; +} + +.spacer { + flex: 1 1 0; + min-height: var(--spacing-lg); +} + +.logo { + display: block; + height: var(--auth-logo-height); + width: auto; + max-width: 220px; + margin: 0 auto; + object-fit: contain; +} + +.facebookButton { + display: inline-flex; + align-items: center; + justify-content: center; gap: var(--spacing-md); width: 100%; + height: var(--auth-field-height); + padding: 0 var(--spacing-lg); + border: none; + border-radius: var(--radius-xxxl); + background: var(--color-auth-surface); + color: var(--color-auth-text-primary); + font-size: var(--font-size-md); + font-weight: 600; + cursor: pointer; + margin-top: var(--spacing-md); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); + transition: filter 0.15s ease, transform 0.05s ease, opacity 0.15s ease; +} + +.facebookButton:hover:not(:disabled) { + filter: brightness(0.97); +} + +.facebookButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.facebookButton:not(:disabled):active { + transform: scale(0.98); +} + +.facebookIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + flex: 0 0 auto; +} + +.facebookLabel { + flex: 1 1 auto; + text-align: center; +} + +.spinner { + display: inline-block; + width: 18px; + height: 18px; + border-width: 2px; + border-style: solid; + border-color: var(--color-facebook-blue) transparent transparent transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; + flex: 0 0 auto; } .linkButton { + align-self: center; background: none; border: none; - padding: 0; - font-size: var(--font-size-sm); - color: var(--color-accent); - text-align: center; + padding: var(--spacing-md) 0; + font-size: var(--font-size-md); + color: var(--color-auth-text-primary); + text-decoration: underline; + text-underline-offset: 3px; cursor: pointer; transition: opacity 0.15s ease; } .linkButton:hover { - opacity: 0.85; + opacity: 0.8; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } } diff --git a/src/app/auth/components/auth-facebook-panel.tsx b/src/app/auth/components/auth-facebook-panel.tsx index b02e7477..4a98a6ca 100644 --- a/src/app/auth/components/auth-facebook-panel.tsx +++ b/src/app/auth/components/auth-facebook-panel.tsx @@ -1,24 +1,23 @@ "use client"; /** - * Facebook 面板(默认面板) + * Facebook 面板(默认登录面板) * * 原始 Dart: lib/ui/auth/widgets/auth_facebook_panel.dart * - * 行为: - * - 默认显示「Continue with Facebook」+ 「Other sign-in options」入口 - * - 社交登录直接调 `nextauth.facebookLogin()` / `nextauth.googleLogin()`(NextAuth 流程) + * 视觉规格(与 Dart 对齐): + * - Spacer → logo(120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律 + * - Facebook 主按钮:46px 白胶囊 + Facebook "f" 图标(#1877f2)+ "Login with Facebook" + * - 社交登录走 NextAuth:`facebookLogin()` / `googleLogin()` + * - "Other Sign-in Options" 链接打开底部弹层 + * - 弹层 Email 按钮 → 触发 `onSwitchToEmail`(父级派发 `AuthPanelModeChanged`) */ import { useState } from "react"; -import { useAuthState } from "@/stores/auth/auth-context"; import { facebookLogin, googleLogin } from "@/lib/auth/nextauth"; -import { AuthSocialButtons } from "./auth-social-buttons"; -import { AuthDivider } from "./auth-divider"; -import { AuthPrimaryButton } from "./auth-primary-button"; import { AuthErrorMessage } from "./auth-error-message"; -import { AuthOtherOptionsDialog } from "./auth-other-options-dialog"; import { AuthLegalText } from "./auth-legal-text"; +import { AuthOtherOptionsDialog } from "./auth-other-options-dialog"; import styles from "./auth-facebook-panel.module.css"; export interface AuthFacebookPanelProps { @@ -26,65 +25,76 @@ export interface AuthFacebookPanelProps { } export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) { - const state = useAuthState(); const [showOptions, setShowOptions] = useState(false); const [busy, setBusy] = useState(null); + const [error, setError] = useState(null); const handleFacebook = async () => { setBusy("facebook"); + setError(null); const r = await facebookLogin(); setBusy(null); - if (!r.success) console.error("[auth-facebook-panel]", r.error); + if (!r.success) setError(r.error?.message ?? "Facebook login failed"); }; const handleGoogle = async () => { setBusy("google"); + setError(null); const r = await googleLogin(); setBusy(null); - if (!r.success) console.error("[auth-facebook-panel]", r.error); + if (!r.success) setError(r.error?.message ?? "Google login failed"); }; return (
- - + Cozsweet - - - Continue with Email - +
+ + + + +
+ + + setShowOptions(false)} - onFacebook={() => { - setShowOptions(false); - void handleFacebook(); - }} - onGoogle={() => { - setShowOptions(false); - void handleGoogle(); - }} - onApple={() => { - setShowOptions(false); - console.warn("Apple login not implemented"); - }} - onEmail={() => { - setShowOptions(false); - onSwitchToEmail(); - }} - loadingProvider={busy ?? (state.isLoading ? "facebook" : null)} - showApple={false} + mode="facebook" + onFacebook={() => void handleFacebook()} + onGoogle={() => void handleGoogle()} + onEmail={onSwitchToEmail} />
); diff --git a/src/app/auth/components/auth-legal-text.module.css b/src/app/auth/components/auth-legal-text.module.css index 7d0d29b7..1a6c4a4e 100644 --- a/src/app/auth/components/auth-legal-text.module.css +++ b/src/app/auth/components/auth-legal-text.module.css @@ -1,13 +1,53 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_legal_text.dart + * + * 视觉规格(与 Dart 对齐): + * - 行:16px 白色圆形复选框 + 12px 富文本 + * - 复选框选中时 10px 粉色实心圆 + * - 链接加粗(Privacy Policy / Terms of Service) + */ +.row { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + width: 100%; +} + +.checkbox { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--auth-legal-checkbox-size); + height: var(--auth-legal-checkbox-size); + border-radius: 50%; + background: var(--color-auth-surface); + border: 1px solid var(--color-auth-surface); + padding: 0; + cursor: pointer; + margin-top: 2px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06); +} + +.checkboxInner { + display: block; + width: var(--auth-legal-checkbox-inner-size); + height: var(--auth-legal-checkbox-inner-size); + border-radius: 50%; + background: var(--color-auth-legal-check); +} + .text { margin: 0; font-size: var(--font-size-sm); - color: var(--color-text-secondary); - text-align: center; - line-height: 1.5; + line-height: 1.4; + color: var(--color-auth-text-primary); + text-align: left; + flex: 1 1 auto; } .link { - color: var(--color-accent); + color: var(--color-auth-text-primary); + font-weight: 700; text-decoration: none; } diff --git a/src/app/auth/components/auth-legal-text.tsx b/src/app/auth/components/auth-legal-text.tsx index f25cb333..83079e50 100644 --- a/src/app/auth/components/auth-legal-text.tsx +++ b/src/app/auth/components/auth-legal-text.tsx @@ -1,23 +1,57 @@ "use client"; /** - * 法律声明(Terms / Privacy) + * 法律声明(Terms / Privacy + 复选框) * * 原始 Dart: lib/ui/auth/widgets/auth_legal_text.dart + * + * 视觉规格(与 Dart 对齐): + * - 行:16px 白色圆形复选框 + 12px 富文本 + * - 复选框:白底 + 1px 白色边框;选中时 10px 粉色实心圆 + * - 默认选中(仅视觉,不阻塞提交) + * - 链接加粗(Privacy Policy / Terms of Service) + * - 文字深色 */ +import { useState } from "react"; + +import { AppConstants } from "@/core/constants/app_constants"; + import styles from "./auth-legal-text.module.css"; export function AuthLegalText() { + const [checked, setChecked] = useState(true); + return ( -

- By continuing, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy Policy - - . -

+
+ +

+ By continuing, you agree to {AppConstants.appTitle}'s{" "} + + Privacy Policy + {" "} + and{" "} + + Terms of Service + + . +

+
); } diff --git a/src/app/auth/components/auth-other-options-dialog.module.css b/src/app/auth/components/auth-other-options-dialog.module.css index 6220ba13..c8e6a34d 100644 --- a/src/app/auth/components/auth-other-options-dialog.module.css +++ b/src/app/auth/components/auth-other-options-dialog.module.css @@ -1,34 +1,53 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_other_options_dialog.dart + * + * 视觉规格(与 Dart 对齐): + * - drag handle 40×4 灰胶囊 + * - 标题 24px bold 深色居中 + * - 选项按钮 40px 白胶囊 + * - Cancel 14px 深色居中文本按钮 + */ .body { display: flex; flex-direction: column; + align-items: stretch; gap: var(--spacing-md); - padding: var(--spacing-xl); + padding: var(--spacing-md) var(--spacing-xxl) + calc(var(--spacing-xxl) + env(safe-area-inset-bottom)); +} + +.handle { + align-self: center; + width: var(--auth-drag-handle-width); + height: var(--auth-drag-handle-height); + border-radius: 2px; + background: var(--color-auth-drag-handle); + margin-bottom: var(--spacing-sm); } .title { - margin: 0; - font-size: var(--font-size-xl); - font-weight: 600; - color: var(--color-text-primary); + margin: var(--spacing-sm) 0; + font-size: var(--font-size-bottom-sheet-title); + font-weight: 700; + color: var(--color-auth-text-primary); text-align: center; } -.emailButton { - display: inline-flex; - align-items: center; - justify-content: center; +.googleSlot { width: 100%; - height: var(--button-height); - padding: 0 var(--spacing-lg); - border: var(--border-light) solid var(--color-chat-input-border); - border-radius: var(--radius-full); - background: transparent; - color: var(--color-text-primary); - font-size: var(--font-size-md); - cursor: pointer; - transition: background 0.15s ease; } -.emailButton:hover { - background: rgba(255, 255, 255, 0.06); +.cancel { + align-self: center; + background: none; + border: none; + padding: var(--spacing-md) 0; + color: var(--color-auth-text-primary); + font-size: var(--font-size-md); + font-weight: 500; + cursor: pointer; + text-decoration: none; +} + +.cancel:hover { + text-decoration: underline; } diff --git a/src/app/auth/components/auth-other-options-dialog.tsx b/src/app/auth/components/auth-other-options-dialog.tsx index cd8565c5..69183253 100644 --- a/src/app/auth/components/auth-other-options-dialog.tsx +++ b/src/app/auth/components/auth-other-options-dialog.tsx @@ -1,12 +1,23 @@ "use client"; /** - * "其他登录方式" 弹窗 + * "其他登录方式" 底部弹层 * * 原始 Dart: lib/ui/auth/widgets/auth_other_options_dialog.dart + * (showModalBottomSheet:白卡 + 顶部圆角 radius28 + drag handle) + * + * 视觉规格(与 Dart 对齐): + * - 固定底部、宽 100%(≤500px)、顶部圆角 28px + * - drag handle 40×4 灰胶囊 + * - 标题 "Other Sign-in Options" 24px bold 深色 + * - 条件渲染 Email / Facebook 按钮(40px 白胶囊 + 边框 + 图标) + * - 始终渲染 Google 按钮 + * - Cancel 文本按钮 */ -import { Dialog } from "@/app/_components/core/dialog"; -import { AuthSocialButtons } from "./auth-social-buttons"; -import { AuthDivider } from "./auth-divider"; +import type { ReactNode } from "react"; + +import { BottomSheet } from "@/app/_components/core/bottom-sheet"; + +import { AuthSocialButton } from "./auth-social-button"; import styles from "./auth-other-options-dialog.module.css"; export interface AuthOtherOptionsDialogProps { @@ -14,11 +25,11 @@ export interface AuthOtherOptionsDialogProps { onClose: () => void; onFacebook: () => void; onGoogle: () => void; - onApple?: () => void; onEmail: () => void; - loadingProvider?: "facebook" | "google" | "apple" | null; - showApple?: boolean; - googleSlot?: React.ReactNode; + /** 当前面板模式:facebook 模式下显示 Email + Google;email 模式下显示 Facebook + Google。 */ + mode: "facebook" | "email"; + /** 自定义子节点(如嵌入 GIS 按钮) */ + googleSlot?: ReactNode; } export function AuthOtherOptionsDialog({ @@ -26,29 +37,99 @@ export function AuthOtherOptionsDialog({ onClose, onFacebook, onGoogle, - onApple, onEmail, - loadingProvider, - showApple, + mode, googleSlot, }: AuthOtherOptionsDialogProps) { return ( - +
+ -
+ ); } diff --git a/src/app/auth/components/auth-panel.module.css b/src/app/auth/components/auth-panel.module.css index 4e492bd2..2011d5ac 100644 --- a/src/app/auth/components/auth-panel.module.css +++ b/src/app/auth/components/auth-panel.module.css @@ -1,7 +1,42 @@ -.panel { +/* 原始 Dart: lib/ui/auth/widgets/auth_panel.dart + * + * 视觉规格(与 Dart 对齐): + * - panelShell 包裹整个面板(position: relative) + * - 悬浮返回按钮:40×40 白色圆形 + 淡黑阴影,固定 top/left 20px + */ +.panelShell { + position: relative; + flex: 1 1 auto; display: flex; flex-direction: column; - gap: var(--spacing-lg); width: 100%; - padding: var(--spacing-xl); + min-height: 0; +} + +.backButton { + position: absolute; + top: var(--spacing-xl); + left: var(--spacing-xl); + z-index: 2; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--auth-back-button-size); + height: var(--auth-back-button-size); + border: none; + border-radius: 50%; + background: var(--color-auth-surface); + color: var(--color-auth-text-primary); + padding: 0 2px 0 0; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: background 0.15s ease, transform 0.05s ease; +} + +.backButton:hover { + background: rgba(0, 0, 0, 0.04); +} + +.backButton:active { + transform: scale(0.96); } diff --git a/src/app/auth/components/auth-panel.tsx b/src/app/auth/components/auth-panel.tsx index 58a7d372..ab763606 100644 --- a/src/app/auth/components/auth-panel.tsx +++ b/src/app/auth/components/auth-panel.tsx @@ -1,30 +1,74 @@ "use client"; /** - * 认证面板:顶层 switch(Facebook / Email) + * 认证面板:顶层 switch(Facebook / Email)+ 悬浮返回按钮 * * 原始 Dart: lib/ui/auth/widgets/auth_panel.dart + * + * 行为对齐: + * - 状态机:`authPanelMode`("facebook" | "email")由 XState 管理 + * - 切换派发 `AuthPanelModeChanged` + * - 悬浮返回按钮(40×40 白圆,top:20, left:20): + * - facebook 模式:`router.back()` + * - email 模式:派发 `AuthPanelModeChanged(facebook)` + * - Email 面板的弹层 Facebook 按钮同样派发 `AuthPanelModeChanged(facebook)` */ -import { useAuthState } from "@/stores/auth/auth-context"; +import { useRouter } from "next/navigation"; + +import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; -import { AuthFacebookPanel } from "./auth-facebook-panel"; import { AuthEmailPanel } from "./auth-email-panel"; +import { AuthFacebookPanel } from "./auth-facebook-panel"; import styles from "./auth-panel.module.css"; export function AuthPanel() { const state = useAuthState(); + const dispatch = useAuthDispatch(); + const router = useRouter(); + + const switchToEmail = () => + dispatch({ type: "AuthPanelModeChanged", mode: "email" }); + const switchToFacebook = () => + dispatch({ type: "AuthPanelModeChanged", mode: "facebook" }); + + const handleBack = () => { + if (state.authPanelMode === "email") { + switchToFacebook(); + } else { + router.back(); + } + }; + return ( -
+
+ + {state.authPanelMode === "facebook" ? ( - { - // 切换模式由外部 dispatch 触发;这里不直接处理 - // 因为 auth-panel 受 `authPanelMode` 控制,调用方需要 dispatch `AuthPanelModeChanged` - // 但我们目前 AuthFacebookPanel 内部 click "Continue with Email" 直接调用 onSwitchToEmail - // 实际上本组件直接渲染对应 panel + + ) : ( + { + switchToFacebook(); }} /> - ) : ( - undefined} /> )}
); diff --git a/src/app/auth/components/auth-primary-button.module.css b/src/app/auth/components/auth-primary-button.module.css index 481bf6bc..2d2fcf6f 100644 --- a/src/app/auth/components/auth-primary-button.module.css +++ b/src/app/auth/components/auth-primary-button.module.css @@ -1,22 +1,37 @@ +/* 原始 Dart: lib/ui/auth/widgets/auth_primary_button.dart + * + * 视觉规格(与 Dart 对齐): + * - 46px 高 + * - 粉渐变(#f96ADE → #f657A0,左→右) + * - 半径 24 + * - 20% 红阴影(#d00c41 @ 20%) + * - 文字 16px bold 白色 + */ .button { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-sm); width: 100%; - height: var(--button-height); + height: var(--auth-field-height); padding: 0 var(--spacing-lg); border: none; - border-radius: var(--radius-full); - font-size: var(--font-size-md); - font-weight: 600; - color: var(--color-text-primary); + border-radius: var(--radius-xxxl); + background: linear-gradient( + 90deg, + var(--color-auth-primary-gradient-start), + var(--color-auth-primary-gradient-end) + ); + box-shadow: 0 3px 5px var(--color-auth-primary-button-shadow); + color: #ffffff; + font-size: var(--font-size-lg); + font-weight: 700; cursor: pointer; - transition: opacity 0.15s ease, transform 0.05s ease; + transition: filter 0.15s ease, transform 0.05s ease, opacity 0.15s ease; } .button:disabled { - opacity: 0.6; + opacity: 0.7; cursor: not-allowed; } @@ -24,25 +39,18 @@ transform: scale(0.98); } -.accent { - background: linear-gradient( - 90deg, - var(--color-accent), - var(--color-facebook-button-gradient-end) - ); -} - -.primary { - background: var(--color-primary); +.label { + flex: 1 1 auto; + text-align: center; } .spinner { display: inline-block; - width: 16px; - height: 16px; + width: 18px; + height: 18px; border-width: 2px; border-style: solid; - border-color: currentColor transparent transparent transparent; + border-color: #ffffff transparent transparent transparent; border-radius: 50%; animation: spin 0.8s linear infinite; } diff --git a/src/app/auth/components/auth-primary-button.tsx b/src/app/auth/components/auth-primary-button.tsx index 1914ab94..a85f50dd 100644 --- a/src/app/auth/components/auth-primary-button.tsx +++ b/src/app/auth/components/auth-primary-button.tsx @@ -1,8 +1,15 @@ "use client"; /** - * 认证主按钮(粉/蓝渐变) + * 认证主按钮(粉渐变,46px) * * 原始 Dart: lib/ui/auth/widgets/auth_primary_button.dart + * + * 视觉规格(与 Dart 对齐): + * - 46px 高 + * - 粉渐变(#f96ADE → #f657A0) + * - 半径 24 + * - 20% 红阴影(#d00c41 @ 20%) + * - 文字 16px bold 白色 */ import { type ButtonHTMLAttributes, type ReactNode } from "react"; @@ -12,28 +19,21 @@ export interface AuthPrimaryButtonProps extends Omit, "children"> { isLoading?: boolean; children: ReactNode; - variant?: "accent" | "primary"; } export function AuthPrimaryButton({ isLoading, children, - variant = "accent", disabled, className, + type = "button", ...rest }: AuthPrimaryButtonProps) { return ( - ) : null} + - Log In + Login - ); } diff --git a/src/app/auth/components/email-register-form.tsx b/src/app/auth/components/email-register-form.tsx index 617b126d..a47902f7 100644 --- a/src/app/auth/components/email-register-form.tsx +++ b/src/app/auth/components/email-register-form.tsx @@ -3,6 +3,13 @@ * 邮箱注册表单 * * 原始 Dart: lib/ui/auth/widgets/email_register_form.dart + * + * 视觉规格(与 Dart 对齐): + * - 4 字段:username / email / password / confirmPassword(顺序与 Dart 一致) + * - 字段无 label(仅 placeholder) + * - 错误仅以红色横幅显示(AuthErrorMessage),无字段级错误 + * - 切换链接由父级 AuthEmailPanel 渲染 + * - 提交派发 `AuthEmailRegisterSubmitted` */ import { useState } from "react"; @@ -20,13 +27,11 @@ import { AuthErrorMessage } from "./auth-error-message"; import styles from "./email-form.module.css"; export interface EmailRegisterFormProps { - onSwitchToLogin: () => void; globalError?: string | null; isLoading?: boolean; } export function EmailRegisterForm({ - onSwitchToLogin, globalError, isLoading, }: EmailRegisterFormProps) { @@ -35,25 +40,13 @@ export function EmailRegisterForm({ const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - const [errors, setErrors] = useState< - Partial> - >({}); const submit = () => { const emailErr = validateEmail(email); const passwordErr = validatePassword(password); const usernameErr = validateUsername(username); const confirmErr = validateConfirmPassword(password, confirmPassword); - if (emailErr || passwordErr || usernameErr || confirmErr) { - setErrors({ - email: emailErr ?? undefined, - password: passwordErr ?? undefined, - username: usernameErr ?? undefined, - confirmPassword: confirmErr ?? undefined, - }); - return; - } - setErrors({}); + if (emailErr || passwordErr || usernameErr || confirmErr) return; dispatch({ type: "AuthEmailRegisterSubmitted", email, @@ -71,53 +64,38 @@ export function EmailRegisterForm({ submit(); }} > - + - Create Account + Register - ); } diff --git a/src/tokens/colors.css b/src/tokens/colors.css index be8e703b..6af14f1d 100644 --- a/src/tokens/colors.css +++ b/src/tokens/colors.css @@ -53,4 +53,34 @@ /* 半透明 / 模糊 */ --color-blur-background: rgba(13, 11, 20, 0.85); + + /* ==================== Auth 页专用(与 Dart auth_screen.dart 对齐) ==================== */ + /* 浅色表面(Auth 页内容卡片 / 弹层背景) */ + --color-auth-surface: #ffffff; + + /* 弹层内第三方登录按钮边框 */ + --color-auth-border: #d5d7db; + + /* Auth 页输入框占位符 */ + --color-auth-input-placeholder: #999999; + + /* Auth 页文字主色(深色,区别于全局白字) */ + --color-auth-text-primary: #333333; + --color-auth-text-secondary: #9e9e9e; + + /* Auth 输入框阴影(accent #f759a8 10% 透明) */ + --color-auth-input-shadow: rgba(247, 89, 168, 0.10); + + /* Auth 主按钮阴影(#d00c41 20% 透明) */ + --color-auth-primary-button-shadow: rgba(208, 12, 65, 0.20); + + /* Auth 主按钮渐变(与 Dart primaryGradient 对齐) */ + --color-auth-primary-gradient-start: #f96ade; + --color-auth-primary-gradient-end: #f657a0; + + /* Legal 复选框选中点 */ + --color-auth-legal-check: #f657a0; + + /* 底部弹层 drag handle */ + --color-auth-drag-handle: #3a3a3a; } diff --git a/src/tokens/dimensions.css b/src/tokens/dimensions.css index c32e00a3..341a1763 100644 --- a/src/tokens/dimensions.css +++ b/src/tokens/dimensions.css @@ -32,4 +32,31 @@ /* 聊天媒体 */ --chat-media-max-width: 240px; --chat-media-max-height: 240px; + + /* ==================== Auth 页尺寸(与 Dart AppDimensions / Spacing 对齐) ==================== */ + /* 输入框 / 主按钮高度(46px = Dart AppDimensions.height46) */ + --auth-field-height: 46px; + + /* 弹层内第三方登录按钮高度(40px) */ + --auth-social-button-height: 40px; + + /* 主按钮固定宽度(Dart AppDimensions.buttonWidth = 334) */ + --auth-primary-button-width: 334px; + + /* Logo 高度(120px) */ + --auth-logo-height: 120px; + + /* Legal 圆形复选框 */ + --auth-legal-checkbox-size: 16px; + --auth-legal-checkbox-inner-size: 10px; + + /* 悬浮返回按钮(40×40 圆形) */ + --auth-back-button-size: 40px; + + /* 底部弹层圆角(28px = Dart AppRadius.radius28) */ + --auth-bottom-sheet-radius: 28px; + + /* 弹层 drag handle(40×4 胶囊) */ + --auth-drag-handle-width: 40px; + --auth-drag-handle-height: 4px; } diff --git a/src/tokens/radius.css b/src/tokens/radius.css index f96096c8..75998fb5 100644 --- a/src/tokens/radius.css +++ b/src/tokens/radius.css @@ -10,4 +10,5 @@ --radius-xxl: 20px; --radius-xxxl: 24px; --radius-full: 999px; + --radius-bottom-sheet: 28px; } diff --git a/src/tokens/typography.css b/src/tokens/typography.css index c4efb0d9..320c436f 100644 --- a/src/tokens/typography.css +++ b/src/tokens/typography.css @@ -28,4 +28,7 @@ /* 扩展字号(迁移自 Flutter widgets) */ --font-size-22: 22px; --font-size-26: 26px; + + /* Auth 底部弹层标题(24px bold) */ + --font-size-bottom-sheet-title: 24px; }