refactor(app): migrate small components to tailwind

This commit is contained in:
2026-07-13 10:22:10 +08:00
parent 965e159a95
commit da04eaa372
9 changed files with 77 additions and 306 deletions
@@ -2,6 +2,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { ChatSendButton } from "../chat-send-button";
import { ImageBubble } from "../image-bubble";
import { MessageAvatar } from "../message-avatar";
import { PrivateMessageCard } from "../private-message-card";
import { TextBubble } from "../text-bubble";
@@ -90,4 +91,29 @@ describe("chat Tailwind components", () => {
expect(emptyHtml).toContain("text-[rgba(255,255,255,0.88)]");
expect(disabledHtml).toContain("disabled");
});
it("renders ImageBubble openable and paywalled states", () => {
const openableHtml = renderToStaticMarkup(
<ImageBubble
messageId="message-1"
imageUrl="/chat-image.png"
onOpenImage={() => undefined}
/>,
);
const paywalledHtml = renderToStaticMarkup(
<ImageBubble imageUrl="/locked-image.png" imagePaywalled />,
);
expect(openableHtml).toContain('role="button"');
expect(openableHtml).toContain('tabindex="0"');
expect(openableHtml).toContain('aria-label="Open image in fullscreen"');
expect(openableHtml).toContain("rounded-tl-none");
expect(openableHtml).toContain("block h-auto w-full object-cover");
expect(openableHtml).toContain("data:image/png;base64,/chat-image.png");
expect(paywalledHtml).toContain("scale-[1.04] blur-[8px]");
expect(paywalledHtml).toContain(
"bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.14)_100%)]",
);
expect(paywalledHtml).toContain("data:image/png;base64,/locked-image.png");
});
});
@@ -1,47 +0,0 @@
/* ImageBubble 图片气泡样式 */
.bubble {
position: relative;
max-width: var(--chat-media-size, 220px);
max-height: var(--chat-media-size, 220px);
border-radius: var(--radius-lg, 12px);
border-top-left-radius: 0;
overflow: hidden;
cursor: pointer;
background: var(--color-bubble-background, #fff);
}
.image {
display: block;
width: 100%;
height: auto;
object-fit: cover;
}
.paywalled .image {
filter: blur(8px);
transform: scale(1.04);
}
.paywallOverlay {
position: absolute;
inset: 0;
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.04) 0%,
rgba(255, 255, 255, 0.14) 100%
);
pointer-events: none;
}
.errorFallback {
display: flex;
align-items: center;
justify-content: center;
width: var(--chat-media-size, 220px);
height: var(--chat-media-size, 220px);
background: var(--color-bubble-background, #fff);
color: var(--color-text-secondary, #9e9e9e);
font-size: var(--icon-size-xl, 24px);
}
+13 -5
View File
@@ -9,7 +9,6 @@
*/
import { ChatMediaImage } from "./chat-media-image";
import styles from "./image-bubble.module.css";
export interface ImageBubbleProps {
messageId?: string;
@@ -25,6 +24,12 @@ export function ImageBubble({
onOpenImage,
}: ImageBubbleProps) {
const canOpen = Boolean(messageId && onOpenImage);
const imageClassName = [
"block h-auto w-full object-cover",
imagePaywalled ? "scale-[1.04] blur-[8px]" : "",
]
.filter(Boolean)
.join(" ");
const openImage = () => {
if (!messageId || !onOpenImage) return;
@@ -33,7 +38,7 @@ export function ImageBubble({
return (
<div
className={`${styles.bubble} ${imagePaywalled ? styles.paywalled : ""}`}
className="relative max-h-[var(--chat-media-size,220px)] max-w-[var(--chat-media-size,220px)] cursor-pointer overflow-hidden rounded-[var(--radius-lg,12px)] rounded-tl-none bg-[var(--color-bubble-background,#fff)]"
onClick={openImage}
role={canOpen ? "button" : undefined}
tabIndex={canOpen ? 0 : undefined}
@@ -49,13 +54,16 @@ export function ImageBubble({
<ChatMediaImage
messageId={messageId}
remoteUrl={imageUrl}
className={styles.image}
errorClassName={styles.errorFallback}
className={imageClassName}
errorClassName="flex size-[var(--chat-media-size,220px)] items-center justify-center bg-[var(--color-bubble-background,#fff)] text-[var(--icon-size-xl,24px)] text-[var(--color-text-secondary,#9e9e9e)]"
width={240}
height={240}
>
{imagePaywalled ? (
<div className={styles.paywallOverlay} aria-hidden="true" />
<div
className="pointer-events-none absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.14)_100%)]"
aria-hidden="true"
/>
) : null}
</ChatMediaImage>
</div>