diff --git a/src/app/_components/core/__tests__/tailwind-components.test.tsx b/src/app/_components/core/__tests__/tailwind-components.test.tsx
index 849ddef0..2c686720 100644
--- a/src/app/_components/core/__tests__/tailwind-components.test.tsx
+++ b/src/app/_components/core/__tests__/tailwind-components.test.tsx
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { Checkbox } from "../checkbox";
import { LoadingIndicator } from "../loading-indicator";
import { PageLoadingFallback } from "../page-loading-fallback";
+import { ResponsiveMobileShell } from "../responsive-mobile-shell";
describe("core Tailwind components", () => {
it("renders LoadingIndicator with accessible status and dynamic sizing", () => {
@@ -54,4 +55,22 @@ describe("core Tailwind components", () => {
expect(labeledHtml).toContain("gap-(--spacing-sm)");
});
+ it("renders ResponsiveMobileShell with bounded canvas utilities", () => {
+ const html = renderToStaticMarkup(
+
+ Content
+ ,
+ );
+
+ expect(html).toContain("min-h-(--app-viewport-height,100dvh)");
+ expect(html).toContain("max-w-(--app-max-width,540px)");
+ expect(html).toContain("overflow-x-clip");
+ expect(html).toContain("custom-content");
+ expect(html).toContain("Content");
+ });
+
});
diff --git a/src/app/_components/core/responsive-mobile-shell.tsx b/src/app/_components/core/responsive-mobile-shell.tsx
index 55f7e2a4..d17887db 100644
--- a/src/app/_components/core/responsive-mobile-shell.tsx
+++ b/src/app/_components/core/responsive-mobile-shell.tsx
@@ -1,7 +1,5 @@
import type { CSSProperties, ReactNode } from "react";
-import styles from "./responsive-mobile-shell.module.css";
-
export interface ResponsiveMobileShellProps {
children: ReactNode;
/** Background used inside the mobile canvas. */
@@ -24,8 +22,18 @@ export function ResponsiveMobileShell({
} as CSSProperties;
return (
-
-
+
diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx
index d89b6fc2..462e6928 100644
--- a/src/app/chat/chat-screen.tsx
+++ b/src/app/chat/chat-screen.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useMemo } from "react";
+import { useEffect, useMemo, type CSSProperties } from "react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
@@ -35,7 +35,11 @@ import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-
import { useChatGuestLogin } from "./hooks/use-chat-guest-login";
import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner";
import { useChatPromotionBootstrap } from "./hooks/use-chat-promotion-bootstrap";
-import styles from "./components/chat-screen.module.css";
+
+const chatShellStyle = {
+ "--chat-message-font-size": "clamp(16px, 3.333vw, 18px)",
+ "--chat-message-line-height": "1.5",
+} as CSSProperties;
export function ChatScreen() {
const router = useRouter();
@@ -188,8 +192,11 @@ export function ChatScreen() {
return (
-
-
+
+
-
+
{/* isGuest 派生自 auth.loginStatus */}
{
+ it("renders guest, member, and VIP header states", () => {
+ const guestHtml = renderToStaticMarkup(
+ undefined}
+ />,
+ );
+ const memberHtml = renderToStaticMarkup(
+ ,
+ );
+ const vipHtml = renderToStaticMarkup(
+ ,
+ );
+
+ expect(guestHtml).toContain("flex w-full items-center");
+ expect(guestHtml).toContain('aria-label="Log in"');
+ expect(guestHtml).toContain("login");
+ expect(memberHtml).toContain("VIP membership not activated");
+ expect(vipHtml).toContain("VIP Member");
+ expect(vipHtml).toContain("rounded-full");
+ });
+
+ it("renders the loading skeleton with stable utility dimensions", () => {
+ const html = renderToStaticMarkup(
+ ,
+ );
+
+ expect(html).toContain('aria-hidden="true"');
+ expect(html).toContain("w-[60%]");
+ expect(html).toContain("w-[80%]");
+ expect(html).toContain("w-[40%]");
+ });
+});
diff --git a/src/app/sidebar/components/user-header.module.css b/src/app/sidebar/components/user-header.module.css
deleted file mode 100644
index 0b233973..00000000
--- a/src/app/sidebar/components/user-header.module.css
+++ /dev/null
@@ -1,132 +0,0 @@
-.row {
- display: flex;
- align-items: center;
- gap: var(--spacing-md, 12px);
- width: 100%;
- box-sizing: border-box;
-}
-
-.avatar {
- flex: 0 0 auto;
- width: var(--sidebar-avatar-size, 56px);
- height: var(--sidebar-avatar-size, 56px);
- border-radius: 50%;
- background: var(--color-accent, #f84d96);
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- color: #ffffff;
-}
-
-.avatarImg {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
-}
-
-.avatarIcon {
- color: inherit;
-}
-
-.text {
- display: flex;
- flex-direction: column;
- gap: var(--sidebar-card-gap-y, 4px);
- flex: 1 1 auto;
- min-width: 0;
-}
-
-.name {
- margin: 0;
- font-size: var(--font-responsive-md, 16px);
- font-weight: 600;
- color: #000000;
- line-height: 1.2;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.subtitle {
- margin: 0;
- font-size: clamp(12px, 2.593vw, 14px);
- color: var(--color-text-secondary, #9e9e9e);
- line-height: 1.2;
-}
-
-/* "VIP Member" 粉色 pill(含小钻石图标) */
-.vipMemberPill {
- display: inline-flex;
- align-items: center;
- gap: var(--spacing-xs, 4px);
- align-self: flex-start;
- padding: var(--responsive-pill-padding-sm, 3px 8px);
- border-radius: var(--radius-full, 999px);
- background: var(--color-pill-bg, rgba(248, 77, 150, 0.10));
- color: var(--color-accent, #f84d96);
- font-size: var(--font-size-xs, 10px);
- font-weight: 600;
- line-height: 1;
-}
-
-.diamondIcon {
- width: clamp(12px, 2.593vw, 14px);
- height: clamp(11px, 2.407vw, 13px);
- display: block;
- flex-shrink: 0;
-}
-
-/* guest 状态下的 "login" 粉色胶囊按钮 */
-.loginPill {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: clamp(5px, 1.111vw, 6px) var(--spacing-lg, 16px);
- border-radius: var(--radius-full, 999px);
- background: linear-gradient(
- 90deg,
- var(--color-button-gradient-start, #ff67e0),
- var(--color-button-gradient-end, #ff52a2)
- );
- color: var(--color-text-primary, #ffffff);
- font-size: clamp(12px, 2.593vw, 14px);
- font-weight: 600;
- border: 0;
- cursor: pointer;
- flex-shrink: 0;
-}
-
-.loginPill:hover {
- opacity: 0.9;
-}
-
-.loginPill:focus-visible {
- outline: 2px solid var(--color-accent, #f84d96);
- outline-offset: 2px;
-}
-
-/* ============================================================
- * 骨架占位(isLoading 时显示)
- * ============================================================ */
-.skeleton {
- display: block;
- background: var(--color-dark-gray, #e5e5e5);
- border-radius: var(--radius-md, 8px);
-}
-
-.skeletonMd {
- width: 60%;
- height: clamp(16px, 3.333vw, 18px);
-}
-
-.skeletonSm {
- width: 80%;
- height: clamp(12px, 2.593vw, 14px);
-}
-
-.skeletonXs {
- width: 40%;
- height: clamp(12px, 2.593vw, 14px);
-}
diff --git a/src/app/sidebar/components/user-header.tsx b/src/app/sidebar/components/user-header.tsx
index b715e91b..ac39a1d0 100644
--- a/src/app/sidebar/components/user-header.tsx
+++ b/src/app/sidebar/components/user-header.tsx
@@ -5,8 +5,6 @@ import Image from "next/image";
import { UserMessageAvatar } from "@/app/_components";
import type { SidebarUserState } from "@/app/sidebar/sidebar-view-model";
-import styles from "./user-header.module.css";
-
export interface UserHeaderProps {
/** 派生状态:未登录 / 已登录未 VIP / 已登录且 VIP */
state: SidebarUserState;
@@ -37,38 +35,42 @@ export function UserHeader({
}: UserHeaderProps) {
if (isLoading) {
return (
-
-
-
-
-
-
+
);
}
return (
-
+
-
-
{name}
+
+
+ {name}
+
{state === "member" && (
-
VIP membership not activated
+
+ VIP membership not activated
+
)}
{state === "vip" && (
-
+
VIP Member
@@ -78,7 +80,7 @@ export function UserHeader({
{state === "guest" && (