diff --git a/src/app/_components/core/__tests__/tailwind-components.test.tsx b/src/app/_components/core/__tests__/tailwind-components.test.tsx index 181231fc..63c4ed15 100644 --- a/src/app/_components/core/__tests__/tailwind-components.test.tsx +++ b/src/app/_components/core/__tests__/tailwind-components.test.tsx @@ -1,6 +1,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; +import { Checkbox } from "../checkbox"; import { LoadingIndicator } from "../loading-indicator"; import { PageLoadingFallback } from "../page-loading-fallback"; @@ -32,4 +33,25 @@ describe("core Tailwind components", () => { expect(html).toContain("text-[rgba(255,255,255,0.72)]"); expect(html).toContain("Loading chat..."); }); + + it("renders Checkbox with checked and labeled states", () => { + const checkedHtml = renderToStaticMarkup( + undefined} ariaLabel="Accept terms" />, + ); + const labeledHtml = renderToStaticMarkup( + undefined} label="Keep me signed in" />, + ); + + expect(checkedHtml).toContain('role="checkbox"'); + expect(checkedHtml).toContain('aria-checked="true"'); + expect(checkedHtml).toContain('aria-label="Accept terms"'); + expect(checkedHtml).toContain("size-[var(--auth-legal-checkbox-size)]"); + expect(checkedHtml).toContain( + "size-[var(--auth-legal-checkbox-inner-size)]", + ); + expect(labeledHtml).toContain(" void; @@ -30,6 +28,9 @@ export function Checkbox({ className, ariaLabel, }: CheckboxProps) { + const checkboxClassName = [ + "mt-0.5 inline-flex size-[var(--auth-legal-checkbox-size)] shrink-0 cursor-pointer items-center justify-center rounded-full border border-[var(--color-auth-surface)] bg-[var(--color-auth-surface)] p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-50", + ].join(" "); const button = ( ); @@ -56,10 +57,17 @@ export function Checkbox({ return ( ); } diff --git a/src/app/_components/core/index.ts b/src/app/_components/core/index.ts index e6046cf6..b4312bfd 100644 --- a/src/app/_components/core/index.ts +++ b/src/app/_components/core/index.ts @@ -11,4 +11,3 @@ export * from "./mobile-shell"; export * from "./page-loading-fallback"; export * from "./page-scaffold"; export * from "./responsive-mobile-shell"; -export * from "./settings-section"; diff --git a/src/app/_components/core/settings-section.module.css b/src/app/_components/core/settings-section.module.css deleted file mode 100644 index 26f3acd7..00000000 --- a/src/app/_components/core/settings-section.module.css +++ /dev/null @@ -1,107 +0,0 @@ -.section { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} - -.title { - margin: 0; - padding: 0 var(--spacing-xs); - font-size: var(--font-size-md); - font-weight: 600; - color: #000000; - text-transform: none; - letter-spacing: 0; -} - -.list { - margin: 0; - padding: 0; - list-style: none; - background: var(--color-settings-card-background, #ffffff); - border-radius: var(--radius-lg, 12px); - overflow: hidden; -} - -.list > li { - background: var(--color-settings-row-background, #f5f5f5); -} - -.row { - display: flex; - align-items: center; - gap: var(--spacing-md); - width: 100%; - padding: var(--spacing-md); - border: none; - background: transparent; - color: #000000; - font-size: var(--font-size-lg); - text-align: left; - cursor: pointer; - transition: background 0.15s ease; -} - -.row:hover { - background: rgba(0, 0, 0, 0.04); -} - -.row:focus-visible { - outline: var(--border-medium) solid var(--color-accent); - outline-offset: -2px; -} - -.icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--responsive-icon-size-md, 24px); - height: var(--responsive-icon-size-md, 24px); - color: var(--color-chevron-icon, #000000); - flex-shrink: 0; -} - -.body { - display: flex; - flex-direction: column; - gap: 2px; - flex: 1 1 auto; - min-width: 0; -} - -.rowTitle { - font-size: var(--font-size-lg); - font-weight: 400; - color: #000000; -} - -.subtitle { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); -} - -.trailing { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - flex-shrink: 0; -} - -.chevron { - color: var(--color-chevron-icon, #000000); - flex-shrink: 0; - width: 20px; - height: 20px; -} - -.divider { - height: 1px; - background: rgba(61, 49, 74, 0.5); - margin: 0 var(--spacing-md); -} - -.destructive { - color: var(--color-destructive, #ff6b6b); -} -.destructive .rowTitle { - color: var(--color-destructive, #ff6b6b); -} diff --git a/src/app/_components/core/settings-section.tsx b/src/app/_components/core/settings-section.tsx deleted file mode 100644 index 1bb6256b..00000000 --- a/src/app/_components/core/settings-section.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; -/** - * 设置项列表 Section - * - * - * - * 渲染一组 SettingItem:图标 + 标题 + 副标题/值 + 右侧 chevron + 可选 divider。 - */ -import { type ReactNode } from "react"; - -import styles from "./settings-section.module.css"; - -export interface SettingsItemModel { - id: string; - title: string; - subtitle?: string; - trailing?: string; - icon?: ReactNode; - onClick?: () => void; - destructive?: boolean; -} - -export interface SettingsSectionProps { - title?: string; - items: SettingsItemModel[]; -} - -export function SettingsSection({ title, items }: SettingsSectionProps) { - return ( -
- {title ?

{title}

: null} -
    - {items.map((item, idx) => ( -
  • - - {idx < items.length - 1 ? ( -
    - ) : null} -
  • - ))} -
-
- ); -} diff --git a/src/app/chat/components/__tests__/tailwind-components.test.tsx b/src/app/chat/components/__tests__/tailwind-components.test.tsx index 630f048f..4fe5e2d5 100644 --- a/src/app/chat/components/__tests__/tailwind-components.test.tsx +++ b/src/app/chat/components/__tests__/tailwind-components.test.tsx @@ -2,6 +2,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { ChatSendButton } from "../chat-send-button"; +import { ImageBubble } from "../image-bubble"; import { MessageAvatar } from "../message-avatar"; import { PrivateMessageCard } from "../private-message-card"; import { TextBubble } from "../text-bubble"; @@ -90,4 +91,29 @@ describe("chat Tailwind components", () => { expect(emptyHtml).toContain("text-[rgba(255,255,255,0.88)]"); expect(disabledHtml).toContain("disabled"); }); + + it("renders ImageBubble openable and paywalled states", () => { + const openableHtml = renderToStaticMarkup( + undefined} + />, + ); + const paywalledHtml = renderToStaticMarkup( + , + ); + + expect(openableHtml).toContain('role="button"'); + expect(openableHtml).toContain('tabindex="0"'); + expect(openableHtml).toContain('aria-label="Open image in fullscreen"'); + expect(openableHtml).toContain("rounded-tl-none"); + expect(openableHtml).toContain("block h-auto w-full object-cover"); + expect(openableHtml).toContain("data:image/png;base64,/chat-image.png"); + expect(paywalledHtml).toContain("scale-[1.04] blur-[8px]"); + expect(paywalledHtml).toContain( + "bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.14)_100%)]", + ); + expect(paywalledHtml).toContain("data:image/png;base64,/locked-image.png"); + }); }); diff --git a/src/app/chat/components/image-bubble.module.css b/src/app/chat/components/image-bubble.module.css deleted file mode 100644 index 8d76c93a..00000000 --- a/src/app/chat/components/image-bubble.module.css +++ /dev/null @@ -1,47 +0,0 @@ -/* ImageBubble 图片气泡样式 */ - -.bubble { - position: relative; - max-width: var(--chat-media-size, 220px); - max-height: var(--chat-media-size, 220px); - border-radius: var(--radius-lg, 12px); - border-top-left-radius: 0; - overflow: hidden; - cursor: pointer; - background: var(--color-bubble-background, #fff); -} - -.image { - display: block; - width: 100%; - height: auto; - object-fit: cover; -} - -.paywalled .image { - filter: blur(8px); - transform: scale(1.04); -} - -.paywallOverlay { - position: absolute; - inset: 0; - background: - linear-gradient( - 180deg, - rgba(255, 255, 255, 0.04) 0%, - rgba(255, 255, 255, 0.14) 100% - ); - pointer-events: none; -} - -.errorFallback { - display: flex; - align-items: center; - justify-content: center; - width: var(--chat-media-size, 220px); - height: var(--chat-media-size, 220px); - background: var(--color-bubble-background, #fff); - color: var(--color-text-secondary, #9e9e9e); - font-size: var(--icon-size-xl, 24px); -} diff --git a/src/app/chat/components/image-bubble.tsx b/src/app/chat/components/image-bubble.tsx index 13053369..db3d473b 100644 --- a/src/app/chat/components/image-bubble.tsx +++ b/src/app/chat/components/image-bubble.tsx @@ -9,7 +9,6 @@ */ import { ChatMediaImage } from "./chat-media-image"; -import styles from "./image-bubble.module.css"; export interface ImageBubbleProps { messageId?: string; @@ -25,6 +24,12 @@ export function ImageBubble({ onOpenImage, }: ImageBubbleProps) { const canOpen = Boolean(messageId && onOpenImage); + const imageClassName = [ + "block h-auto w-full object-cover", + imagePaywalled ? "scale-[1.04] blur-[8px]" : "", + ] + .filter(Boolean) + .join(" "); const openImage = () => { if (!messageId || !onOpenImage) return; @@ -33,7 +38,7 @@ export function ImageBubble({ return (
{imagePaywalled ? ( -