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
@@ -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,
);
}