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
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -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);
}
}
+75
View File
@@ -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(
<div
className={styles.scrim}
style={{ background: `rgba(0, 0, 0, ${scrimOpacity})` }}
onClick={(e) => {
if (!persistent && e.target === e.currentTarget) onClose();
}}
>
<div
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
className={styles.panel}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body,
);
}
@@ -1,21 +1,75 @@
/* 原始 Dart: lib/ui/auth/widgets/auth_email_panel.dart
*
* 视觉规格(与 Dart 对齐):
* - 顶部:login 模式有 logo + 24px spacerregister 模式两个 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;
}
+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>
);
}
@@ -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;
}
@@ -1,21 +1,111 @@
/* 原始 Dart: lib/ui/auth/widgets/auth_facebook_panel.dart
*
* 视觉规格(与 Dart 对齐):
* - Spacer → logo120h)→ 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);
}
}
+50 -40
View File
@@ -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 → logo120h)→ 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 | "facebook" | "google">(null);
const [error, setError] = useState<string | null>(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 (
<div className={styles.panel}>
<AuthErrorMessage message={state.errorMessage} />
<AuthSocialButtons
loadingProvider={busy ?? (state.isLoading ? "facebook" : null)}
onFacebook={handleFacebook}
onGoogle={handleGoogle}
<div className={styles.spacer} />
<img
src="/images/auth/ic-logo-login.png"
alt="Cozsweet"
className={styles.logo}
/>
<AuthDivider label="or" />
<AuthPrimaryButton onClick={onSwitchToEmail} variant="primary">
Continue with Email
</AuthPrimaryButton>
<div className={styles.spacer} />
<button
type="button"
className={styles.facebookButton}
onClick={() => void handleFacebook()}
disabled={busy !== null}
>
{busy === "facebook" ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
<span className={styles.facebookIcon} aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="#1877f2">
<path d="M13 22v-8h3l1-4h-4V7.5c0-1.2.4-2 2-2h2V2.2C16.6 2.1 15.5 2 14.3 2 11.5 2 9.5 3.7 9.5 7v3h-3v4h3v8h3.5z" />
</svg>
</span>
)}
<span className={styles.facebookLabel}>
{busy === "facebook" ? "Logging in..." : "Login with Facebook"}
</span>
</button>
<button
type="button"
className={styles.linkButton}
onClick={() => setShowOptions(true)}
>
Other sign-in options
Other Sign In Options
</button>
<div className={styles.spacer} />
<AuthErrorMessage message={error} />
<AuthLegalText />
<AuthOtherOptionsDialog
open={showOptions}
onClose={() => 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}
/>
</div>
);
@@ -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;
}
+40 -6
View File
@@ -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 (
<div className={styles.row}>
<button
type="button"
className={styles.checkbox}
onClick={() => setChecked((c) => !c)}
aria-label="Agree to terms"
aria-pressed={checked}
>
{checked ? <span className={styles.checkboxInner} /> : null}
</button>
<p className={styles.text}>
By continuing, you agree to our{" "}
<a href="/terms" className={styles.link} target="_blank" rel="noreferrer">
Terms
By continuing, you agree to {AppConstants.appTitle}&apos;s{" "}
<a
href={AppConstants.privacyPolicyUrl}
className={styles.link}
target="_blank"
rel="noreferrer"
>
Privacy Policy
</a>{" "}
and{" "}
<a href="/privacy" className={styles.link} target="_blank" rel="noreferrer">
Privacy Policy
<a
href={AppConstants.termsOfServiceUrl}
className={styles.link}
target="_blank"
rel="noreferrer"
>
Terms of Service
</a>
.
</p>
</div>
);
}
@@ -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;
}
@@ -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 + Googleemail 模式下显示 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 (
<Dialog open={open} onClose={onClose} ariaLabel="Other sign-in options">
<BottomSheet
open={open}
onClose={onClose}
ariaLabel="Other sign-in options"
>
<div className={styles.body}>
<div className={styles.handle} aria-hidden="true" />
<h2 className={styles.title}>Other Sign-in Options</h2>
<AuthSocialButtons
onFacebook={onFacebook}
onGoogle={onGoogle}
onApple={onApple}
loadingProvider={loadingProvider}
showApple={showApple}
googleSlot={googleSlot}
{mode === "facebook" ? (
<AuthSocialButton
icon={
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z" />
</svg>
}
onClick={() => {
onClose();
onEmail();
}}
>
Email Sign In / Register
</AuthSocialButton>
) : (
<AuthSocialButton
icon={
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="var(--color-facebook-blue)"
aria-hidden="true"
>
<path d="M13 22v-8h3l1-4h-4V7.5c0-1.2.4-2 2-2h2V2.2C16.6 2.1 15.5 2 14.3 2 11.5 2 9.5 3.7 9.5 7v3h-3v4h3v8h3.5z" />
</svg>
}
onClick={() => {
onClose();
onFacebook();
}}
>
Facebook Sign In
</AuthSocialButton>
)}
{googleSlot ? (
<div className={styles.googleSlot}>{googleSlot}</div>
) : (
<AuthSocialButton
icon={
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M22 12.2c0-.7-.1-1.4-.2-2H12v3.8h5.6c-.2 1.3-1 2.4-2 3.1v2.6h3.2c1.9-1.7 2.9-4.2 2.9-7.5z"
fill="#4285F4"
/>
<AuthDivider label="or" />
<button type="button" className={styles.emailButton} onClick={onEmail}>
Continue with Email
<path
d="M12 22c2.7 0 4.9-.9 6.5-2.4l-3.2-2.5c-.9.6-2 1-3.3 1-2.6 0-4.7-1.7-5.5-4.1H3.2v2.5C4.8 19.7 8.1 22 12 22z"
fill="#34A853"
/>
<path
d="M6.5 14a6 6 0 010-3.9V7.5H3.2a10 10 0 000 9l3.3-2.5z"
fill="#FBBC05"
/>
<path
d="M12 5.9c1.5 0 2.8.5 3.8 1.5l2.9-2.9C16.9 2.9 14.7 2 12 2 8.1 2 4.8 4.3 3.2 7.5L6.5 10c.8-2.4 2.9-4.1 5.5-4.1z"
fill="#EA4335"
/>
</svg>
}
onClick={() => {
onClose();
onGoogle();
}}
>
Continue with Google
</AuthSocialButton>
)}
<button type="button" className={styles.cancel} onClick={onClose}>
Cancel
</button>
</div>
</Dialog>
</BottomSheet>
);
}
+38 -3
View File
@@ -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);
}
+56 -12
View File
@@ -1,30 +1,74 @@
"use client";
/**
* 认证面板:顶层 switchFacebook / Email
* 认证面板:顶层 switchFacebook / 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 (
<div className={styles.panel}>
<div className={styles.panelShell}>
<button
type="button"
className={styles.backButton}
onClick={handleBack}
aria-label="Back"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
{state.authPanelMode === "facebook" ? (
<AuthFacebookPanel
onSwitchToEmail={() => {
// 切换模式由外部 dispatch 触发;这里不直接处理
// 因为 auth-panel 受 `authPanelMode` 控制,调用方需要 dispatch `AuthPanelModeChanged`
// 但我们目前 AuthFacebookPanel 内部 click "Continue with Email" 直接调用 onSwitchToEmail
// 实际上本组件直接渲染对应 panel
<AuthFacebookPanel onSwitchToEmail={switchToEmail} />
) : (
<AuthEmailPanel
onSwitchToFacebook={() => {
switchToFacebook();
}}
/>
) : (
<AuthEmailPanel onBack={() => undefined} />
)}
</div>
);
@@ -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;
}
+11 -11
View File
@@ -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<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
isLoading?: boolean;
children: ReactNode;
variant?: "accent" | "primary";
}
export function AuthPrimaryButton({
isLoading,
children,
variant = "accent",
disabled,
className,
type = "button",
...rest
}: AuthPrimaryButtonProps) {
return (
<button
type="button"
type={type}
disabled={disabled || isLoading}
className={[
styles.button,
styles[variant],
className,
]
.filter(Boolean)
.join(" ")}
className={[styles.button, className].filter(Boolean).join(" ")}
{...rest}
>
{isLoading ? (
@@ -0,0 +1,23 @@
/* 原始 Dart: lib/ui/auth/auth_screen.dart
*
* 视觉规格(与 Dart 对齐):
* - 全屏 `bg_login.png` 背景(cover
* - 500px 内容列由 MobileShell 约束
* - 内容列居中
*/
.fullBleed {
position: relative;
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 100dvh;
background: url("/images/auth/bg-login.png") center / cover no-repeat;
}
.content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--spacing-lg);
}
+7 -17
View File
@@ -6,6 +6,7 @@
*
* 行为对齐:
* - 500px 移动端宽度(MobileShell
* - 全屏 `bg_login.png` 背景(cover 模式,扩展到 viewport
* - 已登录用户:自动跳 /chat
* - 未登录:渲染 AuthPanel(含 Facebook/Email 两种面板)
* - 登录成功:通知 ChatBloc + UserBloc,然后 router.replace(/chat)
@@ -20,14 +21,9 @@ import { ROUTES } from "@/router/routes";
import { MobileShell } from "@/app/_components/core/mobile-shell";
import { AuthPanel } from "@/app/auth/components/auth-panel";
export interface AuthScreenProps {
/** 自定义背景(默认紫粉渐变)。 */
background?: string;
}
import styles from "./auth-screen.module.css";
export function AuthScreen({
background = "var(--color-sidebar-background)",
}: AuthScreenProps) {
export function AuthScreen() {
// ===== 鉴权路由跳转已由 src/router/proxy.ts 集中处理 =====
// 本组件只负责:
// 1. 渲染登录面板
@@ -50,18 +46,12 @@ export function AuthScreen({
}, [state.isSuccess, chatDispatch, userDispatch, router]);
return (
<MobileShell background={background}>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "var(--spacing-lg)",
}}
>
<div className={styles.fullBleed}>
<MobileShell>
<div className={styles.content}>
<AuthPanel />
</div>
</MobileShell>
</div>
);
}
@@ -1,23 +1,33 @@
/* 原始 Dart: lib/ui/auth/widgets/auth_social_button.dart
*
* 视觉规格(与 Dart 弹层按钮对齐):
* - 40px 高
* - 白底 + 1px 边框 `--color-auth-border` (#d5d7db)
* - 半径 20
* - 无阴影
* - 图标 20×20 + 标签
* - 14px 文字深色
*/
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
gap: var(--spacing-md);
width: 100%;
height: var(--button-height);
height: var(--auth-social-button-height);
padding: 0 var(--spacing-lg);
border: var(--border-light) solid var(--color-chat-input-border);
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.04);
color: var(--color-text-primary);
border: 1px solid var(--color-auth-border);
border-radius: var(--radius-xxl);
background: var(--color-auth-surface);
color: var(--color-auth-text-primary);
font-size: var(--font-size-md);
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
transition: background 0.15s ease, opacity 0.15s ease;
}
.button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.02);
}
.button:disabled {
@@ -31,22 +41,7 @@
justify-content: center;
width: 20px;
height: 20px;
}
.facebook {
background: var(--color-facebook-blue);
border-color: transparent;
}
.google {
background: #ffffff;
color: #1f1f1f;
border-color: transparent;
}
.apple {
background: #000000;
border-color: #ffffff;
flex: 0 0 auto;
}
.label {
+16 -8
View File
@@ -1,10 +1,21 @@
"use client";
/**
* 社交登录按钮
* 社交/选项登录按钮(用于底部弹层)
*
* 原始 Dart: lib/ui/auth/widgets/auth_social_button.dart
*
* 视觉规格(与 Dart 弹层按钮对齐):
* - 40px 高
* - 白底 + 1px 边框 `--color-auth-border`
* - 半径 20
* - 无阴影
* - 图标 20×20 + 标签
* - 14px 文字深色
*/
import { type ButtonHTMLAttributes, type ReactNode } from "react";
import {
type ButtonHTMLAttributes,
type ReactNode,
} from "react";
import styles from "./auth-social-button.module.css";
@@ -12,24 +23,21 @@ export interface AuthSocialButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
icon?: ReactNode;
children: ReactNode;
variant?: "facebook" | "google" | "apple" | "default";
}
export function AuthSocialButton({
icon,
children,
variant = "default",
disabled,
className,
type = "button",
...rest
}: AuthSocialButtonProps) {
return (
<button
type="button"
type={type}
disabled={disabled}
className={[styles.button, styles[variant], className]
.filter(Boolean)
.join(" ")}
className={[styles.button, className].filter(Boolean).join(" ")}
{...rest}
>
{icon ? <span className={styles.icon}>{icon}</span> : null}
@@ -1,3 +1,13 @@
/* 原始 Dart: lib/ui/auth/widgets/auth_text_field.dart
*
* 视觉规格(与 Dart 对齐):
* - 46px 高
* - 白底胶囊(半径 24
* - 无边框
* - 10% 粉色阴影(accent #f759a8 @ 10%
* - padding: 0 16 0 24
* - 文字深色 #333,占位符 #999
*/
.wrapper {
display: flex;
flex-direction: column;
@@ -12,32 +22,35 @@
.input {
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-lg);
background: rgba(255, 255, 255, 0.04);
color: var(--color-text-primary);
height: var(--auth-field-height);
padding: 0 var(--spacing-lg) 0 var(--spacing-xxl);
border: none;
border-radius: var(--radius-xxxl);
background: var(--color-auth-surface);
color: var(--color-auth-text-primary);
font-size: var(--font-size-md);
box-shadow: 0 3px 5px var(--color-auth-input-shadow);
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
transition: box-shadow 0.15s ease;
}
.input::placeholder {
color: var(--color-input-placeholder);
color: var(--color-auth-input-placeholder);
}
.input:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(248, 77, 150, 0.18);
box-shadow: 0 3px 5px var(--color-auth-input-shadow),
0 0 0 2px var(--color-accent);
}
.invalid {
border-color: var(--color-error);
box-shadow: 0 3px 5px rgba(255, 77, 79, 0.18),
0 0 0 1px var(--color-error);
}
.invalid:focus {
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.18);
box-shadow: 0 3px 5px rgba(255, 77, 79, 0.18),
0 0 0 2px var(--color-error);
}
.error {
+8 -20
View File
@@ -1,26 +1,14 @@
/* 邮箱登录/注册表单样式
*
* 原始 Dart: lib/ui/auth/widgets/email_login_form.dart
*
* 视觉规格(与 Dart 对齐):
* - 字段间距 12pxDart AppSpacing.md
* - 错误横幅与主按钮之间 12px
*/
.form {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
width: 100%;
}
.linkButton {
background: none;
border: none;
padding: 0;
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
text-align: center;
cursor: pointer;
transition: color 0.15s ease;
}
.linkButton:hover {
color: var(--color-text-primary);
}
.link {
color: var(--color-accent);
font-weight: 600;
}
+12 -36
View File
@@ -3,6 +3,13 @@
* 邮箱登录表单
*
* 原始 Dart: lib/ui/auth/widgets/email_login_form.dart
*
* 视觉规格(与 Dart 对齐):
* - 字段无 label(仅 placeholder
* - 错误仅以红色横幅显示(AuthErrorMessage),无字段级错误
* - 不显示 "Forgot password?" 链接
* - 切换链接由父级 AuthEmailPanel 渲染
* - 提交派发 `AuthEmailLoginSubmitted`
*/
import { useState } from "react";
@@ -18,33 +25,22 @@ import { AuthErrorMessage } from "./auth-error-message";
import styles from "./email-form.module.css";
export interface EmailLoginFormProps {
onSwitchToRegister: () => void;
onForgotPassword?: () => void;
globalError?: string | null;
isLoading?: boolean;
}
export function EmailLoginForm({
onSwitchToRegister,
onForgotPassword,
globalError,
isLoading,
}: EmailLoginFormProps) {
const dispatch = useAuthDispatch();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState<{ email?: string; password?: string }>(
{},
);
const submit = () => {
const emailErr = validateEmail(email);
const passwordErr = validatePassword(password);
if (emailErr || passwordErr) {
setErrors({ email: emailErr ?? undefined, password: passwordErr ?? undefined });
return;
}
setErrors({});
if (emailErr || passwordErr) return;
dispatch({ type: "AuthEmailLoginSubmitted", email, password });
};
@@ -56,45 +52,25 @@ export function EmailLoginForm({
submit();
}}
>
<AuthErrorMessage message={globalError ?? null} />
<AuthTextField
label="Email"
type="email"
value={email}
onChange={setEmail}
placeholder="you@example.com"
placeholder="Email"
autoComplete="email"
errorMessage={errors.email ?? null}
/>
<AuthTextField
label="Password"
type="password"
value={password}
onChange={setPassword}
placeholder="••••••••"
placeholder="Password"
autoComplete="current-password"
errorMessage={errors.password ?? null}
onSubmit={submit}
/>
{onForgotPassword ? (
<button
type="button"
className={styles.linkButton}
onClick={onForgotPassword}
>
Forgot password?
</button>
) : null}
<AuthErrorMessage message={globalError ?? null} />
<AuthPrimaryButton type="submit" isLoading={isLoading}>
Log In
Login
</AuthPrimaryButton>
<button
type="button"
className={styles.linkButton}
onClick={onSwitchToRegister}
>
Don&apos;t have an account? <span className={styles.link}>Sign up</span>
</button>
</form>
);
}
+14 -36
View File
@@ -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<Record<"email" | "password" | "username" | "confirmPassword", string>>
>({});
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();
}}
>
<AuthErrorMessage message={globalError ?? null} />
<AuthTextField
label="Username"
value={username}
onChange={setUsername}
placeholder="cozsweet_user"
placeholder="Username"
autoComplete="username"
errorMessage={errors.username ?? null}
/>
<AuthTextField
label="Email"
type="email"
value={email}
onChange={setEmail}
placeholder="you@example.com"
placeholder="Email"
autoComplete="email"
errorMessage={errors.email ?? null}
/>
<AuthTextField
label="Password"
type="password"
value={password}
onChange={setPassword}
placeholder="••••••••"
placeholder="Password"
autoComplete="new-password"
errorMessage={errors.password ?? null}
/>
<AuthTextField
label="Confirm Password"
type="password"
value={confirmPassword}
onChange={setConfirmPassword}
placeholder="••••••••"
placeholder="Confirm Password"
autoComplete="new-password"
errorMessage={errors.confirmPassword ?? null}
onSubmit={submit}
/>
<AuthErrorMessage message={globalError ?? null} />
<AuthPrimaryButton type="submit" isLoading={isLoading}>
Create Account
Register
</AuthPrimaryButton>
<button
type="button"
className={styles.linkButton}
onClick={onSwitchToLogin}
>
Already have an account? <span className={styles.link}>Log in</span>
</button>
</form>
);
}
+30
View File
@@ -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;
}
+27
View File
@@ -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 handle40×4 胶囊) */
--auth-drag-handle-width: 40px;
--auth-drag-handle-height: 4px;
}
+1
View File
@@ -10,4 +10,5 @@
--radius-xxl: 20px;
--radius-xxxl: 24px;
--radius-full: 999px;
--radius-bottom-sheet: 28px;
}
+3
View File
@@ -28,4 +28,7 @@
/* 扩展字号(迁移自 Flutter widgets */
--font-size-22: 22px;
--font-size-26: 26px;
/* Auth 底部弹层标题(24px bold */
--font-size-bottom-sheet-title: 24px;
}