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`:
|
* 行为对齐 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user