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 { AuthBackground } from "../auth-background";
|
||||
import { AuthEmailPanel } from "../auth-email-panel";
|
||||
import { AuthErrorMessage } from "../auth-error-message";
|
||||
import { AuthFacebookPanel } from "../auth-facebook-panel";
|
||||
import { AuthLegalText } from "../auth-legal-text";
|
||||
import { AuthPrimaryButton } from "../auth-primary-button";
|
||||
import { AuthSocialButton } from "../auth-social-button";
|
||||
import { AuthSocialButtons } from "../auth-social-buttons";
|
||||
import { AuthTextField } from "../auth-text-field";
|
||||
import { EmailLoginForm } from "../email-login-form";
|
||||
import { EmailRegisterForm } from "../email-register-form";
|
||||
|
||||
vi.mock("@/stores/auth/auth-context", () => ({
|
||||
useAuthDispatch: () => vi.fn(),
|
||||
useAuthState: () => ({
|
||||
authPanelMode: "facebook",
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
loginStatus: "notLoggedIn",
|
||||
hasInitialized: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("auth Tailwind components", () => {
|
||||
@@ -23,6 +34,17 @@ describe("auth Tailwind components", () => {
|
||||
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", () => {
|
||||
const html = renderToStaticMarkup(<EmailLoginForm />);
|
||||
|
||||
@@ -64,6 +86,24 @@ describe("auth Tailwind components", () => {
|
||||
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", () => {
|
||||
const html = renderToStaticMarkup(<AuthLegalText />);
|
||||
|
||||
@@ -90,4 +130,25 @@ describe("auth Tailwind components", () => {
|
||||
expect(html).toContain("text-auth-text-primary");
|
||||
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 { EmailLoginForm } from "./email-login-form";
|
||||
import { EmailRegisterForm } from "./email-register-form";
|
||||
import styles from "./auth-email-panel.module.css";
|
||||
|
||||
export interface AuthEmailPanelProps {
|
||||
/** "Other Sign-in Options" 弹层中 Facebook 按钮回调(派发回 facebook 模式) */
|
||||
@@ -44,28 +43,28 @@ export function AuthEmailPanel({
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.topSection}>
|
||||
<div className="flex min-h-0 w-full flex-auto flex-col items-stretch">
|
||||
<div className="flex flex-col items-stretch">
|
||||
{mode === "login" ? (
|
||||
<>
|
||||
<div className={styles.spacer24} />
|
||||
<div className="h-xxl" />
|
||||
<Image
|
||||
src="/images/auth/ic-logo-login.png"
|
||||
alt="Cozsweet"
|
||||
width={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" }}
|
||||
priority
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.spacer24} />
|
||||
<div className={styles.spacer24} />
|
||||
<div className="h-xxl" />
|
||||
<div className="h-xxl" />
|
||||
</>
|
||||
)}
|
||||
<div className={styles.spacer24} />
|
||||
<div className="h-xxl" />
|
||||
</div>
|
||||
|
||||
{mode === "login" ? (
|
||||
@@ -84,7 +83,7 @@ export function AuthEmailPanel({
|
||||
|
||||
<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"))}
|
||||
>
|
||||
{mode === "login"
|
||||
@@ -94,13 +93,13 @@ export function AuthEmailPanel({
|
||||
|
||||
<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)}
|
||||
>
|
||||
Other Sign In Options
|
||||
</button>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||
<AuthLegalText />
|
||||
|
||||
<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 { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
|
||||
import { AuthProviderIcon } from "./auth-provider-icon";
|
||||
import styles from "./auth-facebook-panel.module.css";
|
||||
|
||||
export interface AuthFacebookPanelProps {
|
||||
onSwitchToEmail: () => void;
|
||||
@@ -49,46 +48,52 @@ export function AuthFacebookPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.spacer} />
|
||||
<div className="flex min-h-0 w-full flex-auto flex-col items-stretch">
|
||||
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||
<Image
|
||||
src="/images/auth/ic-logo-login.png"
|
||||
alt="Cozsweet"
|
||||
width={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" }}
|
||||
priority
|
||||
/>
|
||||
<div className={styles.spacer} />
|
||||
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||
|
||||
<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()}
|
||||
disabled={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" />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.facebookLabel}>
|
||||
<span className="flex-none text-center">
|
||||
{isLoading ? "Logging in..." : "Login with Facebook"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<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)}
|
||||
>
|
||||
Other Sign In Options
|
||||
</button>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" />
|
||||
|
||||
<AuthErrorMessage message={errorMessage} />
|
||||
<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 { AuthSocialButton } from "./auth-social-button";
|
||||
import styles from "./auth-other-options-dialog.module.css";
|
||||
|
||||
export interface AuthOtherOptionsDialogProps {
|
||||
open: boolean;
|
||||
@@ -39,9 +38,17 @@ export function AuthOtherOptionsDialog({
|
||||
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>
|
||||
<div
|
||||
className="flex flex-col items-stretch gap-md px-xxl pt-md"
|
||||
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" ? (
|
||||
<AuthSocialButton
|
||||
@@ -68,7 +75,7 @@ export function AuthOtherOptionsDialog({
|
||||
) : null}
|
||||
|
||||
{googleSlot ? (
|
||||
<div className={styles.googleSlot}>{googleSlot}</div>
|
||||
<div className="w-full">{googleSlot}</div>
|
||||
) : (
|
||||
<AuthSocialButton
|
||||
icon={<AuthProviderIcon provider="google" />}
|
||||
@@ -81,7 +88,11 @@ export function AuthOtherOptionsDialog({
|
||||
</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
|
||||
</button>
|
||||
</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 styles from "./auth-primary-button.module.css";
|
||||
|
||||
export interface AuthPrimaryButtonProps
|
||||
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
||||
isLoading?: boolean;
|
||||
@@ -33,13 +31,21 @@ export function AuthPrimaryButton({
|
||||
<button
|
||||
type={type}
|
||||
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}
|
||||
>
|
||||
{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}
|
||||
<span className={styles.label}>{children}</span>
|
||||
<span className="flex-auto text-center">{children}</span>
|
||||
</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 { AuthProviderIcon } from "./auth-provider-icon";
|
||||
import styles from "./auth-social-buttons.module.css";
|
||||
|
||||
export interface AuthSocialButtonsProps {
|
||||
onFacebook: () => void;
|
||||
@@ -29,30 +28,36 @@ export function AuthSocialButtons({
|
||||
showApple = false,
|
||||
googleSlot,
|
||||
}: 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 (
|
||||
<div className={styles.list}>
|
||||
<div className="flex w-full flex-col gap-md">
|
||||
<button
|
||||
type="button"
|
||||
className={[styles.button, styles.facebook].join(" ")}
|
||||
className={`${buttonClassName} border-transparent bg-facebook-blue`}
|
||||
onClick={onFacebook}
|
||||
disabled={loadingProvider !== null && loadingProvider !== "facebook"}
|
||||
>
|
||||
<span className={styles.icon} aria-hidden="true">
|
||||
<span className={iconClassName} aria-hidden="true">
|
||||
<AuthProviderIcon provider="facebook" />
|
||||
</span>
|
||||
<span>Continue with Facebook</span>
|
||||
</button>
|
||||
|
||||
{googleSlot ? (
|
||||
<div className={styles.googleSlot}>{googleSlot}</div>
|
||||
<div className="flex min-h-[var(--button-height)] w-full justify-center">
|
||||
{googleSlot}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={[styles.button, styles.google].join(" ")}
|
||||
className={`${buttonClassName} border-transparent bg-white text-[#1f1f1f]`}
|
||||
onClick={onGoogle}
|
||||
disabled={loadingProvider !== null && loadingProvider !== "google"}
|
||||
>
|
||||
<span className={styles.icon} aria-hidden="true">
|
||||
<span className={iconClassName} aria-hidden="true">
|
||||
<AuthProviderIcon provider="google" />
|
||||
</span>
|
||||
<span>Continue with Google</span>
|
||||
@@ -62,11 +67,11 @@ export function AuthSocialButtons({
|
||||
{showApple && onApple ? (
|
||||
<button
|
||||
type="button"
|
||||
className={[styles.button, styles.apple].join(" ")}
|
||||
className={`${buttonClassName} border-[rgba(255,255,255,0.2)] bg-black text-white`}
|
||||
onClick={onApple}
|
||||
disabled={loadingProvider !== null && loadingProvider !== "apple"}
|
||||
>
|
||||
<span className={styles.icon} aria-hidden="true">
|
||||
<span className={iconClassName} aria-hidden="true">
|
||||
<AuthProviderIcon provider="apple" />
|
||||
</span>
|
||||
<span>Continue with Apple</span>
|
||||
|
||||
Reference in New Issue
Block a user