refactor(components): migrate shared styles to tailwind
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { BackButton } from "../back-button";
|
||||
import { CharacterAvatar } from "../character-avatar";
|
||||
import { UserMessageAvatar } from "../user-message-avatar";
|
||||
|
||||
describe("shared Tailwind components", () => {
|
||||
it("renders BackButton link and action variants with Tailwind classes", () => {
|
||||
const linkHtml = renderToStaticMarkup(
|
||||
<BackButton href="/chat" variant="soft" ariaLabel="Back to chat" />,
|
||||
);
|
||||
const actionHtml = renderToStaticMarkup(
|
||||
<BackButton onClick={() => undefined} variant="ghost" />,
|
||||
);
|
||||
|
||||
expect(linkHtml).toContain('href="/chat"');
|
||||
expect(linkHtml).toContain('aria-label="Back to chat"');
|
||||
expect(linkHtml).toContain("inline-flex");
|
||||
expect(linkHtml).toContain("size-[var(--responsive-icon-button-size,42px)]");
|
||||
expect(linkHtml).toContain("hover:bg-white");
|
||||
expect(actionHtml).toContain("<button");
|
||||
expect(actionHtml).toContain("size-8");
|
||||
expect(actionHtml).toContain("hover:opacity-70");
|
||||
});
|
||||
|
||||
it("renders CharacterAvatar with dynamic sizing and image utilities", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CharacterAvatar size={48} imageSize={96} className="custom-avatar" />,
|
||||
);
|
||||
|
||||
expect(html).toContain("inline-flex");
|
||||
expect(html).toContain("custom-avatar");
|
||||
expect(html).toContain("width:48px");
|
||||
expect(html).toContain("height:48px");
|
||||
expect(html).toContain("size-full object-cover");
|
||||
expect(html).toContain("%2Fimages%2Fchat%2Fpic-chat-elio.png");
|
||||
});
|
||||
|
||||
it("renders UserMessageAvatar custom and fallback images", () => {
|
||||
const userHtml = renderToStaticMarkup(
|
||||
<UserMessageAvatar avatarUrl="/avatar.png" size="3rem" />,
|
||||
);
|
||||
const guestHtml = renderToStaticMarkup(<UserMessageAvatar />);
|
||||
|
||||
expect(userHtml).toContain('aria-label="User avatar"');
|
||||
expect(userHtml).toContain("width:3rem");
|
||||
expect(userHtml).toContain("height:3rem");
|
||||
expect(userHtml).toContain("size-full object-cover");
|
||||
expect(userHtml).toContain("%2Favatar.png");
|
||||
expect(guestHtml).toContain('aria-label="Guest avatar"');
|
||||
expect(guestHtml).toContain("%2Fimages%2Fchat%2Fpic-chat-guest.png");
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
.backButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: var(--color-auth-text-primary);
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.05s ease;
|
||||
}
|
||||
|
||||
.floating {
|
||||
position: absolute;
|
||||
top: calc(var(--spacing-xl, 20px) + var(--app-safe-top, 0px));
|
||||
left: calc(var(--spacing-xl, 20px) + var(--app-safe-left, 0px));
|
||||
z-index: 2;
|
||||
width: var(--back-button-size);
|
||||
height: var(--back-button-size);
|
||||
padding: 0 2px 0 0;
|
||||
background: var(--color-auth-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.soft {
|
||||
width: var(--responsive-icon-button-size, 42px);
|
||||
height: var(--responsive-icon-button-size, 42px);
|
||||
color: #21191d;
|
||||
border: 1px solid rgba(25, 19, 22, 0.08);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 10px 22px rgba(50, 30, 40, 0.08);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #000000;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.floating:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.soft:hover {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 14px 26px rgba(50, 30, 40, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ghost:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.floating:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.soft:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.ghost:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.backButton:focus-visible {
|
||||
outline: 2px solid #f657a0;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import Link, { type LinkProps } from "next/link";
|
||||
|
||||
import styles from "./back-button.module.css";
|
||||
|
||||
interface BackButtonBaseProps {
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
@@ -24,6 +22,18 @@ type BackButtonActionProps = BackButtonBaseProps & {
|
||||
|
||||
export type BackButtonProps = BackButtonLinkProps | BackButtonActionProps;
|
||||
|
||||
const BASE_CLASS_NAME =
|
||||
"inline-flex cursor-pointer items-center justify-center rounded-full border-0 p-0 text-[var(--color-auth-text-primary)] no-underline transition-[background,box-shadow,transform] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]";
|
||||
|
||||
const VARIANT_CLASS_NAMES = {
|
||||
floating:
|
||||
"absolute left-[calc(var(--spacing-xl,20px)+var(--app-safe-left,0px))] top-[calc(var(--spacing-xl,20px)+var(--app-safe-top,0px))] z-[2] size-[var(--back-button-size)] bg-[var(--color-auth-surface)] pr-[2px] shadow-[0_2px_8px_rgba(0,0,0,0.1)] hover:bg-[rgba(0,0,0,0.04)] active:scale-[0.96]",
|
||||
soft:
|
||||
"size-[var(--responsive-icon-button-size,42px)] border border-[rgba(25,19,22,0.08)] bg-[rgba(255,255,255,0.82)] text-[#21191d] shadow-[0_10px_22px_rgba(50,30,40,0.08)] hover:-translate-y-px hover:bg-white hover:shadow-[0_14px_26px_rgba(50,30,40,0.12)] active:translate-y-px",
|
||||
ghost:
|
||||
"size-8 bg-transparent text-black hover:opacity-70 active:scale-[0.96]",
|
||||
} satisfies Record<NonNullable<BackButtonBaseProps["variant"]>, string>;
|
||||
|
||||
export function BackButton({
|
||||
href,
|
||||
onClick,
|
||||
@@ -33,8 +43,8 @@ export function BackButton({
|
||||
variant = "floating",
|
||||
}: BackButtonProps) {
|
||||
const buttonClassName = [
|
||||
styles.backButton,
|
||||
styles[variant],
|
||||
BASE_CLASS_NAME,
|
||||
VARIANT_CLASS_NAMES[variant],
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.avatar {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: 9999px;
|
||||
background: var(--color-avatar-border, #fbf3f5);
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
import Image from "next/image";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import styles from "./character-avatar.module.css";
|
||||
|
||||
export interface CharacterAvatarProps {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
@@ -31,7 +29,12 @@ export function CharacterAvatar({
|
||||
|
||||
return (
|
||||
<span
|
||||
className={[styles.avatar, className].filter(Boolean).join(" ")}
|
||||
className={[
|
||||
"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-[var(--color-avatar-border,#fbf3f5)]",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
style={style}
|
||||
>
|
||||
<Image
|
||||
@@ -40,6 +43,7 @@ export function CharacterAvatar({
|
||||
width={resolvedImageSize}
|
||||
height={resolvedImageSize}
|
||||
priority={priority}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
import styles from "./user-message-avatar.module.css";
|
||||
|
||||
export interface UserMessageAvatarProps {
|
||||
avatarUrl?: string | null;
|
||||
className?: string;
|
||||
@@ -15,7 +13,12 @@ export function UserMessageAvatar({
|
||||
className,
|
||||
size = 43,
|
||||
}: UserMessageAvatarProps) {
|
||||
const avatarClassName = [styles.avatar, className].filter(Boolean).join(" ");
|
||||
const avatarClassName = [
|
||||
"flex 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))]",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const avatarStyle = { width: size, height: size };
|
||||
const imageSize = typeof size === "number" ? size : 64;
|
||||
|
||||
@@ -26,7 +29,13 @@ export function UserMessageAvatar({
|
||||
style={avatarStyle}
|
||||
aria-label="User avatar"
|
||||
>
|
||||
<Image src={avatarUrl} alt="" width={imageSize} height={imageSize} />
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
width={imageSize}
|
||||
height={imageSize}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +51,7 @@ export function UserMessageAvatar({
|
||||
alt="Guest"
|
||||
width={imageSize}
|
||||
height={imageSize}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user