refactor(chat): migrate simple bubble styles to tailwind

This commit is contained in:
2026-07-11 13:31:46 +08:00
parent bbe1825bb3
commit b426cc7a72
5 changed files with 57 additions and 64 deletions
@@ -0,0 +1,40 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { MessageAvatar } from "../message-avatar";
import { TextBubble } from "../text-bubble";
describe("chat Tailwind components", () => {
it("renders MessageAvatar AI and user branches with image utilities", () => {
const aiHtml = renderToStaticMarkup(<MessageAvatar isFromAI={true} />);
const userHtml = renderToStaticMarkup(
<MessageAvatar isFromAI={false} userAvatarUrl="/user-avatar.png" />,
);
expect(aiHtml).toContain('aria-label="AI avatar"');
expect(aiHtml).toContain("size-[var(--chat-avatar-size,43px)]");
expect(aiHtml).toContain("size-full object-cover");
expect(aiHtml).toContain("%2Fimages%2Fchat%2Fpic-chat-elio.png");
expect(userHtml).toContain('aria-label="User avatar"');
expect(userHtml).toContain("%2Fuser-avatar.png");
});
it("renders TextBubble AI and user visual variants", () => {
const aiHtml = renderToStaticMarkup(
<TextBubble content={"hello\nthere"} isFromAI={true} />,
);
const userHtml = renderToStaticMarkup(
<TextBubble content="from user" isFromAI={false} />,
);
expect(aiHtml).toContain("whitespace-pre-wrap");
expect(aiHtml).toContain("rounded-tl-none");
expect(aiHtml).toContain("bg-white");
expect(aiHtml).toContain("hello\nthere");
expect(userHtml).toContain("rounded-tr-none");
expect(userHtml).toContain(
"bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]",
);
expect(userHtml).toContain("from user");
});
});
@@ -1,30 +0,0 @@
/* MessageAvatar 头像样式 */
.avatar {
flex: 0 0 auto;
width: var(--chat-avatar-size, 43px);
height: var(--chat-avatar-size, 43px);
border-radius: 9999px;
overflow: hidden;
border: 2px solid var(--color-avatar-border, #fbf3f5);
box-shadow: var(--shadow-input-box, 0 1px 2px rgba(0, 0, 0, 0.1));
background: var(--color-avatar-border, #fbf3f5);
display: flex;
align-items: center;
justify-content: center;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--icon-size-xl, 24px);
}
+5 -2
View File
@@ -2,23 +2,26 @@
import Image from "next/image"; import Image from "next/image";
import { UserMessageAvatar } from "@/app/_components"; import { UserMessageAvatar } from "@/app/_components";
import styles from "./message-avatar.module.css";
export interface MessageAvatarProps { export interface MessageAvatarProps {
isFromAI: boolean; isFromAI: boolean;
userAvatarUrl?: string | null; userAvatarUrl?: string | null;
} }
const AVATAR_CLASS_NAME =
"flex size-[var(--chat-avatar-size,43px)] shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-[var(--color-avatar-border,#fbf3f5)] bg-[var(--color-avatar-border,#fbf3f5)] shadow-[var(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))]";
export function MessageAvatar({ isFromAI, userAvatarUrl }: MessageAvatarProps) { export function MessageAvatar({ isFromAI, userAvatarUrl }: MessageAvatarProps) {
if (isFromAI) { if (isFromAI) {
return ( return (
<div className={styles.avatar} aria-label="AI avatar"> <div className={AVATAR_CLASS_NAME} aria-label="AI avatar">
<Image <Image
src="/images/chat/pic-chat-elio.png" src="/images/chat/pic-chat-elio.png"
alt="Elio" alt="Elio"
width={43} width={43}
height={43} height={43}
priority priority
className="size-full object-cover"
/> />
</div> </div>
); );
@@ -1,30 +0,0 @@
/* TextBubble 文字气泡样式 */
.bubble {
/* 撑满父(MessageContent column 宽)—— 让气泡占满所在半区 */
width: 100%;
align-self: stretch;
padding: var(--spacing-3, 12px) var(--spacing-4, 16px);
border-radius: var(--radius-xxl, 24px);
font-size: var(--chat-message-font-size, var(--responsive-body, 16px));
line-height: var(--chat-message-line-height, 1.5);
white-space: pre-wrap;
word-break: break-word;
box-shadow: var(--shadow-input-box, 0 1px 2px rgba(0, 0, 0, 0.1));
}
.bubbleAi {
background: #fff;
color: var(--color-text-foreground, #000);
border-top-left-radius: 0;
}
.bubbleUser {
background: linear-gradient(
to right,
var(--color-button-gradient-start, #ff67e0),
var(--color-button-gradient-end, #ff52a2)
);
color: #fff;
border-top-right-radius: 0;
}
+12 -2
View File
@@ -8,17 +8,27 @@
* - AI 消息:白底黑字 + 左上角尖角 * - AI 消息:白底黑字 + 左上角尖角
* - 用户消息:渐变(primaryGradient+ 白字 + 右上角尖角 * - 用户消息:渐变(primaryGradient+ 白字 + 右上角尖角
*/ */
import styles from "./text-bubble.module.css";
export interface TextBubbleProps { export interface TextBubbleProps {
content: string; content: string;
isFromAI: boolean; isFromAI: boolean;
} }
const BASE_CLASS_NAME =
"w-full self-stretch whitespace-pre-wrap break-words rounded-[var(--radius-xxl,24px)] px-[var(--spacing-4,16px)] py-[var(--spacing-3,12px)] text-[length:var(--chat-message-font-size,var(--responsive-body,16px))] leading-[var(--chat-message-line-height,1.5)] shadow-[var(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))]";
const AI_CLASS_NAME =
"rounded-tl-none bg-white text-[var(--color-text-foreground,#000)]";
const USER_CLASS_NAME =
"rounded-tr-none bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-white";
export function TextBubble({ content, isFromAI }: TextBubbleProps) { export function TextBubble({ content, isFromAI }: TextBubbleProps) {
return ( return (
<div <div
className={`${styles.bubble} ${isFromAI ? styles.bubbleAi : styles.bubbleUser}`} className={[BASE_CLASS_NAME, isFromAI ? AI_CLASS_NAME : USER_CLASS_NAME]
.filter(Boolean)
.join(" ")}
> >
{content} {content}
</div> </div>