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 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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user