refactor(app): migrate small styles to tailwind
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
* 行为对齐 Flutter `CircularProgressIndicator`:
|
||||
* - `size`:圆圈直径(默认 32)
|
||||
* - `strokeWidth`:环宽(默认 3)
|
||||
* - `color`:环颜色(默认 `--color-accent`)
|
||||
* - `color`:环颜色(默认 `--color-text-secondary`)
|
||||
* - `label`:可选的可视标签(环形下方文本)
|
||||
*/
|
||||
import styles from "./loading-indicator.module.css";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
export interface LoadingIndicatorProps {
|
||||
size?: number;
|
||||
@@ -25,16 +25,28 @@ export function LoadingIndicator({
|
||||
color,
|
||||
label,
|
||||
}: LoadingIndicatorProps) {
|
||||
const style: React.CSSProperties = {
|
||||
const spinnerColor = color ?? "var(--color-text-secondary)";
|
||||
const style: CSSProperties = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderWidth: strokeWidth,
|
||||
...(color ? { borderTopColor: color } : {}),
|
||||
borderColor: `${spinnerColor} transparent transparent transparent`,
|
||||
};
|
||||
return (
|
||||
<div className={styles.wrapper} role="status" aria-label="Loading">
|
||||
<div className={styles.spinner} style={style} />
|
||||
{label ? <span className={styles.label}>{label}</span> : null}
|
||||
<div
|
||||
className="inline-flex flex-col items-center gap-sm"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
if (!message) return null;
|
||||
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}
|
||||
</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 { PwaInstallDialog } from "./pwa-install-dialog";
|
||||
import styles from "./pwa-install-overlay.module.css";
|
||||
|
||||
const DEVELOPMENT_DIALOG_DELAY_MS = 1000;
|
||||
const PRODUCTION_DIALOG_DELAY_MS = 1500;
|
||||
@@ -74,7 +73,7 @@ export function PwaInstallOverlay({ enabled }: PwaInstallOverlayProps) {
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return <div className={styles.hidden} aria-hidden="true" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function shouldShowPwaInstallDialog(isDevelopment: boolean) {
|
||||
|
||||
Reference in New Issue
Block a user