refactor(app): migrate small styles to tailwind

This commit is contained in:
2026-07-10 18:18:44 +08:00
parent 89e0fb687a
commit 7d29dbd72f
9 changed files with 70 additions and 59 deletions
@@ -0,0 +1,21 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { LoadingIndicator } from "../loading-indicator";
describe("core Tailwind components", () => {
it("renders LoadingIndicator with accessible status and dynamic sizing", () => {
const html = renderToStaticMarkup(
<LoadingIndicator size={24} strokeWidth={2} color="#f00" label="Loading now" />,
);
expect(html).toContain('role="status"');
expect(html).toContain('aria-label="Loading"');
expect(html).toContain("animate-spin");
expect(html).toContain("width:24px");
expect(html).toContain("height:24px");
expect(html).toContain("border-width:2px");
expect(html).toContain("border-color:#f00 transparent transparent transparent");
expect(html).toContain("Loading now");
});
});
@@ -1,26 +0,0 @@
.wrapper {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
}
.spinner {
display: inline-block;
border-style: solid;
border-color: var(--color-text-secondary) transparent transparent
transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.label {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
+19 -7
View File
@@ -7,10 +7,10 @@
* 行为对齐 Flutter `CircularProgressIndicator` * 行为对齐 Flutter `CircularProgressIndicator`
* - `size`:圆圈直径(默认 32 * - `size`:圆圈直径(默认 32
* - `strokeWidth`:环宽(默认 3 * - `strokeWidth`:环宽(默认 3
* - `color`:环颜色(默认 `--color-accent` * - `color`:环颜色(默认 `--color-text-secondary`
* - `label`:可选的可视标签(环形下方文本) * - `label`:可选的可视标签(环形下方文本)
*/ */
import styles from "./loading-indicator.module.css"; import type { CSSProperties } from "react";
export interface LoadingIndicatorProps { export interface LoadingIndicatorProps {
size?: number; size?: number;
@@ -25,16 +25,28 @@ export function LoadingIndicator({
color, color,
label, label,
}: LoadingIndicatorProps) { }: LoadingIndicatorProps) {
const style: React.CSSProperties = { const spinnerColor = color ?? "var(--color-text-secondary)";
const style: CSSProperties = {
width: size, width: size,
height: size, height: size,
borderWidth: strokeWidth, borderWidth: strokeWidth,
...(color ? { borderTopColor: color } : {}), borderColor: `${spinnerColor} transparent transparent transparent`,
}; };
return ( return (
<div className={styles.wrapper} role="status" aria-label="Loading"> <div
<div className={styles.spinner} style={style} /> className="inline-flex flex-col items-center gap-sm"
{label ? <span className={styles.label}>{label}</span> : null} role="status"
aria-label="Loading"
>
<div
className="inline-block animate-spin rounded-full border-solid"
style={style}
/>
{label ? (
<span className="text-[var(--font-size-sm)] text-text-secondary">
{label}
</span>
) : null}
</div> </div>
); );
} }
@@ -0,0 +1,15 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { AuthErrorMessage } from "../auth-error-message";
describe("auth Tailwind components", () => {
it("renders AuthErrorMessage only when a message is present", () => {
expect(renderToStaticMarkup(<AuthErrorMessage message={null} />)).toBe("");
const html = renderToStaticMarkup(<AuthErrorMessage message="Invalid email" />);
expect(html).toContain('role="alert"');
expect(html).toContain("text-error");
expect(html).toContain("Invalid email");
});
});
@@ -1,16 +0,0 @@
/*
*
* 视觉规格(与 Dart 对齐):
* - 细小红字,无背景,无圆角,无边框
* - 12px 居中
*/
.banner {
width: 100%;
padding: 0;
background: transparent;
border: none;
color: var(--color-error);
font-size: var(--responsive-caption, var(--font-size-sm));
line-height: 1.4;
text-align: center;
}
@@ -4,12 +4,13 @@
* *
* *
*/ */
import styles from "./auth-error-message.module.css";
export function AuthErrorMessage({ message }: { message: string | null }) { export function AuthErrorMessage({ message }: { message: string | null }) {
if (!message) return null; if (!message) return null;
return ( return (
<div className={styles.banner} role="alert"> <div
className="w-full bg-transparent p-0 text-center text-[var(--responsive-caption)] leading-[1.4] text-error"
role="alert"
>
{message} {message}
</div> </div>
); );
@@ -0,0 +1,10 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { PwaInstallOverlay } from "../pwa-install-overlay";
describe("PwaInstallOverlay", () => {
it("does not render visible markup", () => {
expect(renderToStaticMarkup(<PwaInstallOverlay enabled />)).toBe("");
});
});
@@ -1,5 +0,0 @@
/* PwaInstallOverlay PWA 安装弹窗触发器样式(无可见 UI,逻辑触发用) */
.hidden {
display: none;
}
@@ -22,7 +22,6 @@ import {
import { pwaUtil } from "@/utils"; import { pwaUtil } from "@/utils";
import { PwaInstallDialog } from "./pwa-install-dialog"; import { PwaInstallDialog } from "./pwa-install-dialog";
import styles from "./pwa-install-overlay.module.css";
const DEVELOPMENT_DIALOG_DELAY_MS = 1000; const DEVELOPMENT_DIALOG_DELAY_MS = 1000;
const PRODUCTION_DIALOG_DELAY_MS = 1500; const PRODUCTION_DIALOG_DELAY_MS = 1500;
@@ -74,7 +73,7 @@ export function PwaInstallOverlay({ enabled }: PwaInstallOverlayProps) {
}; };
}, [enabled]); }, [enabled]);
return <div className={styles.hidden} aria-hidden="true" />; return null;
} }
async function shouldShowPwaInstallDialog(isDevelopment: boolean) { async function shouldShowPwaInstallDialog(isDevelopment: boolean) {