diff --git a/public/images/auth/ic-logo-login.png b/public/images/auth/ic-logo-login.png new file mode 100644 index 00000000..d5b13922 Binary files /dev/null and b/public/images/auth/ic-logo-login.png differ 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 00000000..5bf0e279 Binary files /dev/null and b/public/images/auth/ic-logo-login@2x.png differ 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 00000000..8f1c7084 Binary files /dev/null and b/public/images/auth/ic-logo-login@3x.png differ 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; }