refactor(chat): migrate simple bubble styles to tailwind
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -2,23 +2,26 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { UserMessageAvatar } from "@/app/_components";
|
||||
import styles from "./message-avatar.module.css";
|
||||
|
||||
export interface MessageAvatarProps {
|
||||
isFromAI: boolean;
|
||||
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) {
|
||||
if (isFromAI) {
|
||||
return (
|
||||
<div className={styles.avatar} aria-label="AI avatar">
|
||||
<div className={AVATAR_CLASS_NAME} aria-label="AI avatar">
|
||||
<Image
|
||||
src="/images/chat/pic-chat-elio.png"
|
||||
alt="Elio"
|
||||
width={43}
|
||||
height={43}
|
||||
priority
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</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;
|
||||
}
|
||||
@@ -8,17 +8,27 @@
|
||||
* - AI 消息:白底黑字 + 左上角尖角
|
||||
* - 用户消息:渐变(primaryGradient)+ 白字 + 右上角尖角
|
||||
*/
|
||||
import styles from "./text-bubble.module.css";
|
||||
|
||||
export interface TextBubbleProps {
|
||||
content: string;
|
||||
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) {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.bubble} ${isFromAI ? styles.bubbleAi : styles.bubbleUser}`}
|
||||
className={[BASE_CLASS_NAME, isFromAI ? AI_CLASS_NAME : USER_CLASS_NAME]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user