refactor(auth): migrate form controls to tailwind

This commit is contained in:
2026-07-10 19:05:51 +08:00
parent a33c77ecfb
commit b7c4ba9b4e
7 changed files with 80 additions and 186 deletions
@@ -3,6 +3,9 @@ import { describe, expect, it, vi } from "vitest";
import { AuthBackground } from "../auth-background";
import { AuthErrorMessage } from "../auth-error-message";
import { AuthLegalText } from "../auth-legal-text";
import { AuthSocialButton } from "../auth-social-button";
import { AuthTextField } from "../auth-text-field";
import { EmailLoginForm } from "../email-login-form";
import { EmailRegisterForm } from "../email-register-form";
@@ -49,4 +52,42 @@ describe("auth Tailwind components", () => {
expect(html).toContain("object-cover");
expect(html).toContain("%2Fimages%2Fauth%2Fbg-login.png");
});
it("renders AuthSocialButton with Tailwind button layout", () => {
const html = renderToStaticMarkup(
<AuthSocialButton icon={<span>G</span>}>Continue</AuthSocialButton>,
);
expect(html).toContain("min-h-[var(--auth-social-button-height)]");
expect(html).toContain("border-auth-border");
expect(html).toContain("text-auth-text-primary");
expect(html).toContain("Continue");
});
it("renders AuthLegalText with Tailwind checkbox and links", () => {
const html = renderToStaticMarkup(<AuthLegalText />);
expect(html).toContain('aria-label="Agree to terms"');
expect(html).toContain("size-[var(--auth-legal-checkbox-size)]");
expect(html).toContain("bg-auth-legal-check");
expect(html).toContain("Privacy Agreement");
expect(html).toContain("Terms of User");
});
it("renders AuthTextField with Tailwind input states", () => {
const html = renderToStaticMarkup(
<AuthTextField
value="bad"
onChange={() => {}}
label="Email"
placeholder="Email"
errorMessage="Invalid email"
/>,
);
expect(html).toContain("min-h-[var(--auth-field-height)]");
expect(html).toContain("placeholder:text-auth-input-placeholder");
expect(html).toContain("text-auth-text-primary");
expect(html).toContain("Invalid email");
});
});
@@ -1,56 +0,0 @@
/*
*
* 视觉规格(与 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(--responsive-caption, var(--font-size-sm));
line-height: 1.4;
color: var(--color-auth-text-primary);
text-align: left;
flex: 1 1 auto;
}
.link {
color: var(--color-auth-text-primary);
font-weight: 700;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
+8 -8
View File
@@ -15,27 +15,27 @@ import { useState } from "react";
import { AppConstants } from "@/core/app_constants";
import styles from "./auth-legal-text.module.css";
export function AuthLegalText() {
const [checked, setChecked] = useState(true);
return (
<div className={styles.row}>
<div className="flex w-full items-start gap-sm">
<button
type="button"
className={styles.checkbox}
className="mt-0.5 inline-flex size-[var(--auth-legal-checkbox-size)] flex-none cursor-pointer items-center justify-center rounded-full border border-auth-surface bg-auth-surface p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]"
onClick={() => setChecked((c) => !c)}
aria-label="Agree to terms"
aria-pressed={checked}
>
{checked ? <span className={styles.checkboxInner} /> : null}
{checked ? (
<span className="block size-[var(--auth-legal-checkbox-inner-size)] rounded-full bg-auth-legal-check" />
) : null}
</button>
<p className={styles.text}>
<p className="m-0 flex-auto text-left text-[var(--responsive-caption)] leading-[1.4] text-auth-text-primary">
I agree to the Cozsweet {" "}
<a
href={AppConstants.privacyPolicyUrl}
className={styles.link}
className="font-bold text-auth-text-primary no-underline hover:underline"
target="_blank"
rel="noreferrer"
>
@@ -44,7 +44,7 @@ export function AuthLegalText() {
and{" "}
<a
href={AppConstants.termsOfServiceUrl}
className={styles.link}
className="font-bold text-auth-text-primary no-underline hover:underline"
target="_blank"
rel="noreferrer"
>
@@ -1,50 +0,0 @@
/*
*
* 视觉规格(与 Dart 弹层按钮对齐):
* - 40px 高
* - 白底 + 1px 边框 `--color-auth-border` (#d5d7db)
* - 半径 20
* - 无阴影
* - 图标 20×20 + 标签
* - 14px 文字深色
*/
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
width: 100%;
min-height: var(--auth-social-button-height);
padding: 0 var(--spacing-lg);
border: 1px solid var(--color-auth-border);
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: 500;
cursor: pointer;
transition: background 0.15s ease, opacity 0.15s ease;
}
.button:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.02);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex: 0 0 auto;
}
.label {
flex: 0 0 auto;
text-align: center;
}
+12 -5
View File
@@ -17,8 +17,6 @@ import {
type ReactNode,
} from "react";
import styles from "./auth-social-button.module.css";
export interface AuthSocialButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
icon?: ReactNode;
@@ -37,11 +35,20 @@ export function AuthSocialButton({
<button
type={type}
disabled={disabled}
className={[styles.button, className].filter(Boolean).join(" ")}
className={[
"inline-flex min-h-[var(--auth-social-button-height)] w-full cursor-pointer items-center justify-center gap-md rounded-full border border-auth-border bg-auth-surface px-lg text-[var(--responsive-body)] font-medium text-auth-text-primary transition-[background,opacity] duration-150 enabled:hover:bg-[rgba(0,0,0,0.02)] disabled:cursor-not-allowed disabled:opacity-50",
className,
]
.filter(Boolean)
.join(" ")}
{...rest}
>
{icon ? <span className={styles.icon}>{icon}</span> : null}
<span className={styles.label}>{children}</span>
{icon ? (
<span className="inline-flex size-5 flex-none items-center justify-center">
{icon}
</span>
) : null}
<span className="flex-none text-center">{children}</span>
</button>
);
}
@@ -1,59 +0,0 @@
/*
*
* 视觉规格(与 Dart 对齐):
* - 46px 高
* - 白底胶囊(半径 24
* - 无边框
* - 10% 粉色阴影(accent #f759a8 @ 10%
* - padding: 0 16 0 24
* - 文字深色 #333,占位符 #999
*/
.wrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
width: 100%;
}
.label {
font-size: var(--responsive-caption, var(--font-size-sm));
color: var(--color-text-secondary);
}
.input {
width: 100%;
min-height: var(--auth-field-height);
padding: 0 var(--spacing-lg) 0 var(--spacing-xxl);
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));
box-shadow: 0 3px 5px var(--color-auth-input-shadow);
outline: none;
transition: box-shadow 0.15s ease;
}
.input::placeholder {
color: var(--color-auth-input-placeholder);
}
.input:focus {
box-shadow: 0 3px 5px var(--color-auth-input-shadow),
0 0 0 2px var(--color-accent);
}
.invalid {
box-shadow: 0 3px 5px rgba(255, 77, 79, 0.18),
0 0 0 1px var(--color-error);
}
.invalid:focus {
box-shadow: 0 3px 5px rgba(255, 77, 79, 0.18),
0 0 0 2px var(--color-error);
}
.error {
font-size: var(--responsive-caption, var(--font-size-sm));
color: var(--color-error);
}
+19 -8
View File
@@ -11,8 +11,6 @@ import {
forwardRef,
} from "react";
import styles from "./auth-text-field.module.css";
export interface AuthTextFieldProps {
value: string;
onChange: (value: string) => void;
@@ -44,14 +42,25 @@ export const AuthTextField = forwardRef<HTMLInputElement, AuthTextFieldProps>(
},
ref,
) {
const inputClassName = [
"min-h-[var(--auth-field-height)] w-full rounded-full border-0 bg-auth-surface py-0 pl-xxl pr-lg text-[var(--responsive-body)] text-auth-text-primary shadow-[0_3px_5px_var(--color-auth-input-shadow)] outline-none transition-shadow duration-150 placeholder:text-auth-input-placeholder focus:shadow-[0_3px_5px_var(--color-auth-input-shadow),0_0_0_2px_var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-70",
errorMessage
? "shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_1px_var(--color-error)] focus:shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_2px_var(--color-error)]"
: "",
]
.filter(Boolean)
.join(" ");
return (
<label className={styles.wrapper}>
{label ? <span className={styles.label}>{label}</span> : null}
<label className="flex w-full flex-col gap-xs">
{label ? (
<span className="text-[var(--responsive-caption)] text-text-secondary">
{label}
</span>
) : null}
<input
ref={ref}
className={[styles.input, errorMessage ? styles.invalid : ""]
.filter(Boolean)
.join(" ")}
className={inputClassName}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
@@ -70,7 +79,9 @@ export const AuthTextField = forwardRef<HTMLInputElement, AuthTextFieldProps>(
}}
/>
{errorMessage ? (
<span className={styles.error}>{errorMessage}</span>
<span className="text-[var(--responsive-caption)] text-error">
{errorMessage}
</span>
) : null}
</label>
);