diff --git a/src/app/_components/__tests__/tailwind-components.test.tsx b/src/app/_components/__tests__/tailwind-components.test.tsx new file mode 100644 index 00000000..8bd0a995 --- /dev/null +++ b/src/app/_components/__tests__/tailwind-components.test.tsx @@ -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( + , + ); + const actionHtml = renderToStaticMarkup( + 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(" { + const html = renderToStaticMarkup( + , + ); + + 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( + , + ); + const guestHtml = renderToStaticMarkup(); + + 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"); + }); +}); diff --git a/src/app/_components/back-button.module.css b/src/app/_components/back-button.module.css deleted file mode 100644 index 564dde76..00000000 --- a/src/app/_components/back-button.module.css +++ /dev/null @@ -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; -} diff --git a/src/app/_components/back-button.tsx b/src/app/_components/back-button.tsx index 66382989..beabf6b2 100644 --- a/src/app/_components/back-button.tsx +++ b/src/app/_components/back-button.tsx @@ -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, 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) diff --git a/src/app/_components/character-avatar.module.css b/src/app/_components/character-avatar.module.css deleted file mode 100644 index 1801bd23..00000000 --- a/src/app/_components/character-avatar.module.css +++ /dev/null @@ -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; -} diff --git a/src/app/_components/character-avatar.tsx b/src/app/_components/character-avatar.tsx index 15992929..c88d5204 100644 --- a/src/app/_components/character-avatar.tsx +++ b/src/app/_components/character-avatar.tsx @@ -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 ( ); diff --git a/src/app/_components/user-message-avatar.module.css b/src/app/_components/user-message-avatar.module.css deleted file mode 100644 index c267dc38..00000000 --- a/src/app/_components/user-message-avatar.module.css +++ /dev/null @@ -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; -} diff --git a/src/app/_components/user-message-avatar.tsx b/src/app/_components/user-message-avatar.tsx index 64e592cd..6da0e669 100644 --- a/src/app/_components/user-message-avatar.tsx +++ b/src/app/_components/user-message-avatar.tsx @@ -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" > - + ); } @@ -42,6 +51,7 @@ export function UserMessageAvatar({ alt="Guest" width={imageSize} height={imageSize} + className="size-full object-cover" /> );