refactor(auth): migrate remaining styles to tailwind
This commit is contained in:
@@ -2,15 +2,26 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { AuthBackground } from "../auth-background";
|
import { AuthBackground } from "../auth-background";
|
||||||
|
import { AuthEmailPanel } from "../auth-email-panel";
|
||||||
import { AuthErrorMessage } from "../auth-error-message";
|
import { AuthErrorMessage } from "../auth-error-message";
|
||||||
|
import { AuthFacebookPanel } from "../auth-facebook-panel";
|
||||||
import { AuthLegalText } from "../auth-legal-text";
|
import { AuthLegalText } from "../auth-legal-text";
|
||||||
|
import { AuthPrimaryButton } from "../auth-primary-button";
|
||||||
import { AuthSocialButton } from "../auth-social-button";
|
import { AuthSocialButton } from "../auth-social-button";
|
||||||
|
import { AuthSocialButtons } from "../auth-social-buttons";
|
||||||
import { AuthTextField } from "../auth-text-field";
|
import { AuthTextField } from "../auth-text-field";
|
||||||
import { EmailLoginForm } from "../email-login-form";
|
import { EmailLoginForm } from "../email-login-form";
|
||||||
import { EmailRegisterForm } from "../email-register-form";
|
import { EmailRegisterForm } from "../email-register-form";
|
||||||
|
|
||||||
vi.mock("@/stores/auth/auth-context", () => ({
|
vi.mock("@/stores/auth/auth-context", () => ({
|
||||||
useAuthDispatch: () => vi.fn(),
|
useAuthDispatch: () => vi.fn(),
|
||||||
|
useAuthState: () => ({
|
||||||
|
authPanelMode: "facebook",
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
loginStatus: "notLoggedIn",
|
||||||
|
hasInitialized: true,
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("auth Tailwind components", () => {
|
describe("auth Tailwind components", () => {
|
||||||
@@ -23,6 +34,17 @@ describe("auth Tailwind components", () => {
|
|||||||
expect(html).toContain("Invalid email");
|
expect(html).toContain("Invalid email");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders AuthPrimaryButton with Tailwind gradient and spinner", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AuthPrimaryButton isLoading>Continue</AuthPrimaryButton>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("min-h-[var(--auth-field-height)]");
|
||||||
|
expect(html).toContain("bg-[linear-gradient");
|
||||||
|
expect(html).toContain("animate-spin");
|
||||||
|
expect(html).toContain("Continue");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders EmailLoginForm with Tailwind form layout", () => {
|
it("renders EmailLoginForm with Tailwind form layout", () => {
|
||||||
const html = renderToStaticMarkup(<EmailLoginForm />);
|
const html = renderToStaticMarkup(<EmailLoginForm />);
|
||||||
|
|
||||||
@@ -64,6 +86,24 @@ describe("auth Tailwind components", () => {
|
|||||||
expect(html).toContain("Continue");
|
expect(html).toContain("Continue");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders AuthSocialButtons with provider-specific Tailwind styles", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AuthSocialButtons
|
||||||
|
onFacebook={() => {}}
|
||||||
|
onGoogle={() => {}}
|
||||||
|
onApple={() => {}}
|
||||||
|
showApple
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("bg-facebook-blue");
|
||||||
|
expect(html).toContain("bg-white");
|
||||||
|
expect(html).toContain("bg-black");
|
||||||
|
expect(html).toContain("Continue with Facebook");
|
||||||
|
expect(html).toContain("Continue with Google");
|
||||||
|
expect(html).toContain("Continue with Apple");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders AuthLegalText with Tailwind checkbox and links", () => {
|
it("renders AuthLegalText with Tailwind checkbox and links", () => {
|
||||||
const html = renderToStaticMarkup(<AuthLegalText />);
|
const html = renderToStaticMarkup(<AuthLegalText />);
|
||||||
|
|
||||||
@@ -90,4 +130,25 @@ describe("auth Tailwind components", () => {
|
|||||||
expect(html).toContain("text-auth-text-primary");
|
expect(html).toContain("text-auth-text-primary");
|
||||||
expect(html).toContain("Invalid email");
|
expect(html).toContain("Invalid email");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders AuthEmailPanel with Tailwind panel layout", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AuthEmailPanel onSwitchToFacebook={() => {}} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("flex min-h-0 w-full flex-auto flex-col");
|
||||||
|
expect(html).toContain("h-xxl");
|
||||||
|
expect(html).toContain("Other Sign In Options");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders AuthFacebookPanel with Tailwind Facebook button", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<AuthFacebookPanel onSwitchToEmail={() => {}} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain("Login with Facebook");
|
||||||
|
expect(html).toContain("bg-auth-surface");
|
||||||
|
expect(html).toContain("enabled:hover:brightness-[0.97]");
|
||||||
|
expect(html).toContain("Other Sign In Options");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* 视觉规格(与 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;
|
|
||||||
align-items: stretch;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topSection {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer24 {
|
|
||||||
height: var(--spacing-xxl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: block;
|
|
||||||
height: clamp(92px, 22.222vw, var(--auth-logo-height));
|
|
||||||
width: auto;
|
|
||||||
max-width: min(56vw, 220px);
|
|
||||||
margin: 0 auto;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex: 1 1 0;
|
|
||||||
min-height: var(--page-section-gap, var(--spacing-lg));
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleButton {
|
|
||||||
align-self: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: var(--spacing-md) 0 0;
|
|
||||||
font-size: var(--responsive-body, var(--font-size-md));
|
|
||||||
color: var(--color-auth-text-primary);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleButton:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linkButton {
|
|
||||||
align-self: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: var(--spacing-md) 0 var(--spacing-md);
|
|
||||||
font-size: var(--responsive-body, 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;
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ import { AuthLegalText } from "./auth-legal-text";
|
|||||||
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
|
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
|
||||||
import { EmailLoginForm } from "./email-login-form";
|
import { EmailLoginForm } from "./email-login-form";
|
||||||
import { EmailRegisterForm } from "./email-register-form";
|
import { EmailRegisterForm } from "./email-register-form";
|
||||||
import styles from "./auth-email-panel.module.css";
|
|
||||||
|
|
||||||
export interface AuthEmailPanelProps {
|
export interface AuthEmailPanelProps {
|
||||||
/** "Other Sign-in Options" 弹层中 Facebook 按钮回调(派发回 facebook 模式) */
|
/** "Other Sign-in Options" 弹层中 Facebook 按钮回调(派发回 facebook 模式) */
|
||||||
@@ -44,28 +43,28 @@ export function AuthEmailPanel({
|
|||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="flex min-h-0 w-full flex-auto flex-col items-stretch">
|
||||||
<div className={styles.topSection}>
|
<div className="flex flex-col items-stretch">
|
||||||
{mode === "login" ? (
|
{mode === "login" ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.spacer24} />
|
<div className="h-xxl" />
|
||||||
<Image
|
<Image
|
||||||
src="/images/auth/ic-logo-login.png"
|
src="/images/auth/ic-logo-login.png"
|
||||||
alt="Cozsweet"
|
alt="Cozsweet"
|
||||||
width={120}
|
width={120}
|
||||||
height={120}
|
height={120}
|
||||||
className={styles.logo}
|
className="mx-auto block h-[clamp(92px,22.222vw,var(--auth-logo-height))] max-w-[min(56vw,220px)] object-contain"
|
||||||
style={{ width: "auto", height: "auto" }}
|
style={{ width: "auto", height: "auto" }}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styles.spacer24} />
|
<div className="h-xxl" />
|
||||||
<div className={styles.spacer24} />
|
<div className="h-xxl" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className={styles.spacer24} />
|
<div className="h-xxl" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === "login" ? (
|
{mode === "login" ? (
|
||||||
@@ -84,7 +83,7 @@ export function AuthEmailPanel({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.toggleButton}
|
className="self-center border-0 bg-transparent pt-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
|
||||||
onClick={() => setMode((m) => (m === "login" ? "register" : "login"))}
|
onClick={() => setMode((m) => (m === "login" ? "register" : "login"))}
|
||||||
>
|
>
|
||||||
{mode === "login"
|
{mode === "login"
|
||||||
@@ -94,13 +93,13 @@ export function AuthEmailPanel({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.linkButton}
|
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
|
||||||
onClick={() => setShowOptions(true)}
|
onClick={() => setShowOptions(true)}
|
||||||
>
|
>
|
||||||
Other Sign In Options
|
Other Sign In Options
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||||
<AuthLegalText />
|
<AuthLegalText />
|
||||||
|
|
||||||
<AuthOtherOptionsDialog
|
<AuthOtherOptionsDialog
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* 视觉规格(与 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(--page-section-gap, var(--spacing-lg));
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: block;
|
|
||||||
height: clamp(92px, 22.222vw, var(--auth-logo-height));
|
|
||||||
width: auto;
|
|
||||||
max-width: min(56vw, 220px);
|
|
||||||
margin: 0 auto;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.facebookButton {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
width: 100%;
|
|
||||||
min-height: var(--auth-field-height);
|
|
||||||
padding: 0 var(--spacing-lg);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-full, 999px);
|
|
||||||
background: var(--color-auth-surface);
|
|
||||||
color: var(--color-auth-text-primary);
|
|
||||||
font-size: var(--responsive-body, 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: 0 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: var(--responsive-spinner-size, 18px);
|
|
||||||
height: var(--responsive-spinner-size, 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: var(--spacing-md) 0;
|
|
||||||
font-size: var(--responsive-body, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import { AuthErrorMessage } from "./auth-error-message";
|
|||||||
import { AuthLegalText } from "./auth-legal-text";
|
import { AuthLegalText } from "./auth-legal-text";
|
||||||
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
|
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
|
||||||
import { AuthProviderIcon } from "./auth-provider-icon";
|
import { AuthProviderIcon } from "./auth-provider-icon";
|
||||||
import styles from "./auth-facebook-panel.module.css";
|
|
||||||
|
|
||||||
export interface AuthFacebookPanelProps {
|
export interface AuthFacebookPanelProps {
|
||||||
onSwitchToEmail: () => void;
|
onSwitchToEmail: () => void;
|
||||||
@@ -49,46 +48,52 @@ export function AuthFacebookPanel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel}>
|
<div className="flex min-h-0 w-full flex-auto flex-col items-stretch">
|
||||||
<div className={styles.spacer} />
|
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||||
<Image
|
<Image
|
||||||
src="/images/auth/ic-logo-login.png"
|
src="/images/auth/ic-logo-login.png"
|
||||||
alt="Cozsweet"
|
alt="Cozsweet"
|
||||||
width={120}
|
width={120}
|
||||||
height={120}
|
height={120}
|
||||||
className={styles.logo}
|
className="mx-auto block h-[clamp(92px,22.222vw,var(--auth-logo-height))] max-w-[min(56vw,220px)] object-contain"
|
||||||
style={{ width: "auto", height: "auto" }}
|
style={{ width: "auto", height: "auto" }}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className={styles.spacer} />
|
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.facebookButton}
|
className="mt-md inline-flex min-h-[var(--auth-field-height)] w-full cursor-pointer items-center justify-center gap-md rounded-full border-0 bg-auth-surface px-lg text-[var(--responsive-body)] font-semibold text-auth-text-primary shadow-[0_2px_6px_rgba(0,0,0,0.06)] transition-[filter,transform,opacity] duration-150 enabled:hover:brightness-[0.97] enabled:active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
onClick={() => handleFacebook()}
|
onClick={() => handleFacebook()}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<span className={styles.spinner} aria-hidden="true" />
|
<span
|
||||||
|
className="inline-block size-[var(--responsive-spinner-size,18px)] flex-none animate-spin rounded-full border-2 border-solid border-facebook-blue border-r-transparent border-b-transparent border-l-transparent"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.facebookIcon} aria-hidden="true">
|
<span
|
||||||
|
className="inline-flex size-5 flex-none items-center justify-center"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<AuthProviderIcon provider="facebook" />
|
<AuthProviderIcon provider="facebook" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={styles.facebookLabel}>
|
<span className="flex-none text-center">
|
||||||
{isLoading ? "Logging in..." : "Login with Facebook"}
|
{isLoading ? "Logging in..." : "Login with Facebook"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.linkButton}
|
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
|
||||||
onClick={() => setShowOptions(true)}
|
onClick={() => setShowOptions(true)}
|
||||||
>
|
>
|
||||||
Other Sign In Options
|
Other Sign In Options
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className={styles.spacer} />
|
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||||
|
|
||||||
<AuthErrorMessage message={errorMessage} />
|
<AuthErrorMessage message={errorMessage} />
|
||||||
<AuthLegalText />
|
<AuthLegalText />
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* 视觉规格(与 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-md) var(--spacing-xxl)
|
|
||||||
calc(var(--spacing-xxl) + var(--app-safe-bottom, 0px));
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: var(--spacing-sm) 0;
|
|
||||||
font-size: var(--responsive-page-title, var(--font-size-bottom-sheet-title));
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-auth-text-primary);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.googleSlot {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel {
|
|
||||||
align-self: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: var(--spacing-md) 0;
|
|
||||||
color: var(--color-auth-text-primary);
|
|
||||||
font-size: var(--responsive-body, var(--font-size-md));
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import { AppEnvUtil } from "@/utils";
|
|||||||
|
|
||||||
import { AuthProviderIcon } from "./auth-provider-icon";
|
import { AuthProviderIcon } from "./auth-provider-icon";
|
||||||
import { AuthSocialButton } from "./auth-social-button";
|
import { AuthSocialButton } from "./auth-social-button";
|
||||||
import styles from "./auth-other-options-dialog.module.css";
|
|
||||||
|
|
||||||
export interface AuthOtherOptionsDialogProps {
|
export interface AuthOtherOptionsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -39,9 +38,17 @@ export function AuthOtherOptionsDialog({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
ariaLabel="Other sign-in options"
|
ariaLabel="Other sign-in options"
|
||||||
>
|
>
|
||||||
<div className={styles.body}>
|
<div
|
||||||
<div className={styles.handle} aria-hidden="true" />
|
className="flex flex-col items-stretch gap-md px-xxl pt-md"
|
||||||
<h2 className={styles.title}>Other Sign-in Options</h2>
|
style={{ paddingBottom: "calc(var(--spacing-xxl) + var(--app-safe-bottom, 0px))" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mb-sm h-[var(--auth-drag-handle-height)] w-[var(--auth-drag-handle-width)] self-center rounded-[2px] bg-auth-drag-handle"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<h2 className="my-sm text-center text-[var(--responsive-page-title)] font-bold text-auth-text-primary">
|
||||||
|
Other Sign-in Options
|
||||||
|
</h2>
|
||||||
|
|
||||||
{mode === "email" ? (
|
{mode === "email" ? (
|
||||||
<AuthSocialButton
|
<AuthSocialButton
|
||||||
@@ -68,7 +75,7 @@ export function AuthOtherOptionsDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{googleSlot ? (
|
{googleSlot ? (
|
||||||
<div className={styles.googleSlot}>{googleSlot}</div>
|
<div className="w-full">{googleSlot}</div>
|
||||||
) : (
|
) : (
|
||||||
<AuthSocialButton
|
<AuthSocialButton
|
||||||
icon={<AuthProviderIcon provider="google" />}
|
icon={<AuthProviderIcon provider="google" />}
|
||||||
@@ -81,7 +88,11 @@ export function AuthOtherOptionsDialog({
|
|||||||
</AuthSocialButton>
|
</AuthSocialButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button type="button" className={styles.cancel} onClick={onClose}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] font-medium text-auth-text-primary no-underline hover:underline"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/*
|
|
||||||
*
|
|
||||||
* 视觉规格(与 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%;
|
|
||||||
min-height: var(--auth-field-height);
|
|
||||||
padding: 0 var(--spacing-lg);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-full, 999px);
|
|
||||||
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(--responsive-body, var(--font-size-lg));
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: filter 0.15s ease, transform 0.05s ease, opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:disabled {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:not(:disabled):active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: var(--responsive-spinner-size, 18px);
|
|
||||||
height: var(--responsive-spinner-size, 18px);
|
|
||||||
border-width: 2px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #ffffff transparent transparent transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,8 +13,6 @@
|
|||||||
*/
|
*/
|
||||||
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
import { type ButtonHTMLAttributes, type ReactNode } from "react";
|
||||||
|
|
||||||
import styles from "./auth-primary-button.module.css";
|
|
||||||
|
|
||||||
export interface AuthPrimaryButtonProps
|
export interface AuthPrimaryButtonProps
|
||||||
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -33,13 +31,21 @@ export function AuthPrimaryButton({
|
|||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
className={[styles.button, className].filter(Boolean).join(" ")}
|
className={[
|
||||||
|
"inline-flex min-h-[var(--auth-field-height)] w-full cursor-pointer items-center justify-center gap-sm rounded-full border-0 bg-[linear-gradient(90deg,var(--color-auth-primary-gradient-start),var(--color-auth-primary-gradient-end))] px-lg text-[var(--responsive-body)] font-bold text-white shadow-[0_3px_5px_var(--color-auth-primary-button-shadow)] transition-[filter,transform,opacity] duration-150 active:enabled:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70",
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<span className={styles.spinner} aria-hidden="true" />
|
<span
|
||||||
|
className="inline-block size-[var(--responsive-spinner-size,18px)] animate-spin rounded-full border-2 border-solid border-white border-r-transparent border-b-transparent border-l-transparent"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span className={styles.label}>{children}</span>
|
<span className="flex-auto text-center">{children}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-md);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
width: 100%;
|
|
||||||
min-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: rgba(255, 255, 255, 0.04);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-size: var(--responsive-body, var(--font-size-md));
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover:not(:disabled) {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
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: rgba(255, 255, 255, 0.2);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.googleSlot {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
min-height: var(--button-height);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
import { AuthProviderIcon } from "./auth-provider-icon";
|
import { AuthProviderIcon } from "./auth-provider-icon";
|
||||||
import styles from "./auth-social-buttons.module.css";
|
|
||||||
|
|
||||||
export interface AuthSocialButtonsProps {
|
export interface AuthSocialButtonsProps {
|
||||||
onFacebook: () => void;
|
onFacebook: () => void;
|
||||||
@@ -29,30 +28,36 @@ export function AuthSocialButtons({
|
|||||||
showApple = false,
|
showApple = false,
|
||||||
googleSlot,
|
googleSlot,
|
||||||
}: AuthSocialButtonsProps) {
|
}: AuthSocialButtonsProps) {
|
||||||
|
const buttonClassName =
|
||||||
|
"inline-flex min-h-[var(--button-height)] w-full cursor-pointer items-center justify-center gap-sm rounded-full border border-chat-input-border bg-[rgba(255,255,255,0.04)] px-lg text-[var(--responsive-body)] font-medium text-text-primary transition-colors duration-150 enabled:hover:bg-[rgba(255,255,255,0.08)] disabled:cursor-not-allowed disabled:opacity-50";
|
||||||
|
const iconClassName = "inline-flex size-5 items-center justify-center";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.list}>
|
<div className="flex w-full flex-col gap-md">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={[styles.button, styles.facebook].join(" ")}
|
className={`${buttonClassName} border-transparent bg-facebook-blue`}
|
||||||
onClick={onFacebook}
|
onClick={onFacebook}
|
||||||
disabled={loadingProvider !== null && loadingProvider !== "facebook"}
|
disabled={loadingProvider !== null && loadingProvider !== "facebook"}
|
||||||
>
|
>
|
||||||
<span className={styles.icon} aria-hidden="true">
|
<span className={iconClassName} aria-hidden="true">
|
||||||
<AuthProviderIcon provider="facebook" />
|
<AuthProviderIcon provider="facebook" />
|
||||||
</span>
|
</span>
|
||||||
<span>Continue with Facebook</span>
|
<span>Continue with Facebook</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{googleSlot ? (
|
{googleSlot ? (
|
||||||
<div className={styles.googleSlot}>{googleSlot}</div>
|
<div className="flex min-h-[var(--button-height)] w-full justify-center">
|
||||||
|
{googleSlot}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={[styles.button, styles.google].join(" ")}
|
className={`${buttonClassName} border-transparent bg-white text-[#1f1f1f]`}
|
||||||
onClick={onGoogle}
|
onClick={onGoogle}
|
||||||
disabled={loadingProvider !== null && loadingProvider !== "google"}
|
disabled={loadingProvider !== null && loadingProvider !== "google"}
|
||||||
>
|
>
|
||||||
<span className={styles.icon} aria-hidden="true">
|
<span className={iconClassName} aria-hidden="true">
|
||||||
<AuthProviderIcon provider="google" />
|
<AuthProviderIcon provider="google" />
|
||||||
</span>
|
</span>
|
||||||
<span>Continue with Google</span>
|
<span>Continue with Google</span>
|
||||||
@@ -62,11 +67,11 @@ export function AuthSocialButtons({
|
|||||||
{showApple && onApple ? (
|
{showApple && onApple ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={[styles.button, styles.apple].join(" ")}
|
className={`${buttonClassName} border-[rgba(255,255,255,0.2)] bg-black text-white`}
|
||||||
onClick={onApple}
|
onClick={onApple}
|
||||||
disabled={loadingProvider !== null && loadingProvider !== "apple"}
|
disabled={loadingProvider !== null && loadingProvider !== "apple"}
|
||||||
>
|
>
|
||||||
<span className={styles.icon} aria-hidden="true">
|
<span className={iconClassName} aria-hidden="true">
|
||||||
<AuthProviderIcon provider="apple" />
|
<AuthProviderIcon provider="apple" />
|
||||||
</span>
|
</span>
|
||||||
<span>Continue with Apple</span>
|
<span>Continue with Apple</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user